嘉立创产业服务站群
发作品签到
专业版

DIY机器狗

工程标签

69
0
0
0

简介

本项目基于ESP8266构建,是一个可以手机控制的桌面宠物,电路大部分使用插件封装,方便焊接。 功能上,它支持手机遥控,可实现表情显示,还能展示每日天气与时钟信息参数配置。

简介:本项目基于ESP8266构建,是一个可以手机控制的桌面宠物,电路大部分使用插件封装,方便焊接。 功能上,它支持手机遥控,可实现表情显示,还能展示每日天气与时钟信息参数配置。
智能机器狗实训营
复刻成本:60

开源协议

CERN Open Hardware License

创建时间:2025-03-24 10:26:32更新时间:2025-04-28 07:50:12

描述

项目简介

本项目基于ESP8266构建,是一个可以手机控制的桌面宠物,电路大部分使用插件封装,方便焊接。 功能上,它支持手机遥控,可实现表情显示,还能展示每日天气与时钟信息参数配置。

项目功能

1.手机控制:通过连接机器狗的WiFi,在操作页面即可对小狗做出控制
83fb3326c08ccf5a337eeb1e3c88db7e.jpg
2.表情显示:可显示开心、喜欢、思考等表情
14a784afc0c6194ef977f6128653e1be.jpg
3.显示每日天气和时钟:可通过这操作页面的联网功能实现

3c0d74b81097d3ca3452109c36248de7.jpg

项目参数

1.本设计采用ESP8266主控,内置WIFI功能,通过AP模式遥控
2.屏幕支持0.96寸
SSD1306/SSD1315驱动OLED显示屏,可显示表情、时钟、天气等相关信息
3.选用AMS1117LDO线性稳压器,负责将8.4V和5V电压分别转换成5V和3.3V,为舵机及主控提供电源
4.项目支持SG-90/MG-90 180度及360度版本,推荐使用180度版本,自带限位器,无需电机校准

原理解析(硬件说明)

本项目由以下部分组成
c5d81579f505c8b1a40a45709c390da0.png

电源电路:
供电:供电是直接采用14500双节电池组,通过LDO降压稳压器供电。

image.png

稳压电路:
稳压器:采用AMS1117-5V和AMS1117-3.3V线性稳压器,将8.4V电压分别转换成5V和3.3V,使其为舵机及ESP8266主控提供电力支持
image.png

主控电路:
主控:参考ESP8266数据手册,对IO0、IO2、EN使能、RST重置引脚上拉,对CS片选信号下拉,以确保ESP8266及SPI通信正常
image.png

外设电路:
屏幕:为了方便焊接,简化电路,这里使用的是SSD1315驱动的OLED屏幕模块,该模块自带有屏幕驱动电路,仅需接口接入即可。在此根据该屏幕模块的接口线序配置好了对应接口的线序,直接插入即可使用。
串口:串口部分为方便下载,单独引出了IO0及GND接口作为跳帽插入接口,当插入跳帽时,IO0被拉低,进入下载模式。反之被主控部分电路拉高,进入工作模式。
电池:电池部分,引出了外部充电拓展接口,VIN与VBAT是开关接口,VIN与GND接口是外部充电模块接口。充电模块选择满电电压大概在8.4V的2串锂电池充电模块。
按键:按键部分使用的是IO2和IO15引脚,IO2按键按下时拉低,空闲时被拉高。但由于IO15必须接下拉电阻,所以这里开关逻辑与IO2相反,按键按下时拉高,空闲时被拉低。

image.png

image.png

![image.png](httpADC电量检测电路
s://image.lceda.cn/oshwhub/pullImage/8190cb17f74d4879a2dd5c9dce3ff36b.png)

image.png
ADC电量检测电路

软件代码

//----------------------------------------------
//EDA-Robot(180度舵机版本)
//CodeVersion:V1.1
//---------------导入库--------------------------
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "image.cpp"
//---------------按键部分--------------------------
#define BUTTON_PIN 2 // GPIO2 引脚 (D4)
#define BUTTON_PIN2 15
volatile bool buttonPressed = false; // 按键标志
volatile bool buttonPressed2 = false; // 按键标志
unsigned long lastPressTime = 0; // 上次按键时间
const unsigned long debounceDelay = 50; // 消抖时间 (ms)
unsigned long lastPressTime2 = 0; // 上次按键时间
const unsigned long debounceDelay2 = 50; // 消抖时间 (ms)
//---------------ADC部分--------------------------
const float voltageDividerRatio = 8.4; // 分压比(8.4倍缩小)
const float minVoltage = 6.4; // 电压为0%时
const float maxVoltage = 8.4; // 电压为100%时
const int numSamples = 10;//定义采用次数
float batteryVoltage = 0; // 计算电池电压
int batteryPercentage = 0;//电量百分比
//---------------舵机部分--------------------------
Servo servo1;//声明舵机1
Servo servo2;//声明舵机2
Servo servo3;//声明舵机3
Servo servo4;//声明舵机4
int engine1 = 14; // 舵机1引脚
int engine2 = 16; // 舵机2引脚
int engine3 = 12; // 舵机3引脚
int engine4 = 13; // 舵机4引脚
//---------------屏幕部分--------------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=/U8X8_PIN_NONE, / clock=/5, / data=*/4); // 使用SSD1306屏幕驱动,时钟引脚5,数据引脚4
//---------------网络部分--------------------------
const char *ssid = "EDA-Robot";//WIFI名称
const char *password = ""; // 无密码
AsyncWebServer server(80);//设置服务器端口
WiFiUDP ntpUDP;//声明UDP
NTPClient timeClient(ntpUDP, "ntp1.aliyun.com", 8 * 3600, 60000);//配置NTP服务器
//---------------API部分--------------------------
const char *weatherAPI = "http://api.seniverse.com/v3/weather/daily.json?key=";//心知天气API地址
String temperature = "";//天气温度
String humidity = "";//天气湿度
String weather = "";//天气
String cityname = "";//城市名称
String weatherapi = "";//心知天气API密钥
//---------------标签部分--------------------------
bool initweather = false; // 天气初始化
bool freestate = false;//自由模式标签
int prevEmojiState = -1; // 用于跟踪之前的 emojiState
int actionstate = 0;//活动状态标签
int emojiState = 0; // 表情状态标签
//---------------文件系统部分--------------------------
const char *ssidFile = "/ssid.json";//配置存储文件名及路径
//---------------按键中断部分--------------------------
void ICACHE_RAM_ATTR handleButtonPress()
{

unsigned long currentTime = millis();// 获取当前系统运行时间(单位:毫秒)

if (currentTime - lastPressTime > debounceDelay) // 检查按钮1是否满足去抖条件(避免机械抖动导致的误触发)
{
    buttonPressed = true;     // 设置按钮1按下标志位
    lastPressTime = currentTime; // 更新按钮1的最后有效按下时间
}
unsigned long currentTime2 = millis();// 获取当前时间
if (currentTime2 - lastPressTime2 > debounceDelay2)// 检查按钮2的去抖条件(使用独立的去抖时间和记录变量)
{
    buttonPressed2 = true;    // 设置按钮2按下标志位
    lastPressTime2 = currentTime2; // 更新按钮2的最后有效按下时间
}

}
//---------------配置页面路由--------------------------
void handleWiFiConfig()
{

server.on("/front", HTTP_GET, [](AsyncWebServerRequest *request) {// 当访问 /front 路径时触发舵机动作
actionstate = 1; // 更新全局动作状态标志(1通常表示前进/前方动作)

request->send(200, "text/plain", "Front function started"); // 立即响应客户端,避免阻塞(状态码200,返回纯文本确认信息)

});
//以下函数相同,不再注释
server.on("/back", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 4; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/left", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 2; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/right", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 3; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/toplefthand", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 5; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/toprighthand", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 6; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/sitdown", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 8; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/lie", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 7;
request->send(200, "text/plain", "Front function started"); });
server.on("/sleep", HTTP_GET, [](AsyncWebServerRequest *request)
{
actionstate = 10;
request->send(200, "text/plain", "Front function started"); });
server.on("/free", HTTP_GET, [](AsyncWebServerRequest *request)
{
freestate=true;
request->send(200, "text/plain", "Front function started"); });
server.on("/offfree", HTTP_GET, [](AsyncWebServerRequest *request)
{
freestate=false;
request->send(200, "text/plain", "Front function started"); });
server.on("/histate", HTTP_GET, [](AsyncWebServerRequest *request)
{
emojiState = 0; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/angrystate", HTTP_GET, [](AsyncWebServerRequest *request)
{
emojiState = 1; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });
server.on("/edastate", HTTP_GET, [](AsyncWebServerRequest *request)
{
emojiState = 9; // 设置标志,执行舵机动作
request->send(200, "text/plain", "Front function started"); });

server.on("/errorstate", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 2;  // 设置标志,执行舵机动作
    request->send(200, "text/plain", "Front function started"); });
server.on("/batteryVoltage", HTTP_GET, [](AsyncWebServerRequest *request)
          { request->send(200, "text/plain", String(batteryVoltage)); });
server.on("/batteryPercentage", HTTP_GET, [](AsyncWebServerRequest *request)
          { request->send(200, "text/plain", String(batteryPercentage)); });
server.on("/dowhatstate", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 3;  // 设置标志,执行舵机动作
    request->send(200, "text/plain", "Front function started"); });
server.on("/lovestate", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 4;  // 设置标志,执行舵机动作
    request->send(200, "text/plain", "Front function started"); });
server.on("/sickstate", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 5;  // 设置标志,执行舵机动作
    request->send(200, "text/plain", "Front function started"); });
server.on("/yunstate", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 6; 
    request->send(200, "text/plain", "Front function started"); });
server.on("/time", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 8; 
    request->send(200, "text/plain", "Front function started"); });
server.on("/weather", HTTP_GET, [](AsyncWebServerRequest *request)
          {
   emojiState = 7;  // 设置标志,执行舵机动作
    request->send(200, "text/plain", "Front function started"); });

server.on("/connect", HTTP_POST, [](AsyncWebServerRequest *request)
          {
    // 获取POST参数:ssid、pass、city、api
    String ssid = request->getParam("ssid", true)->value();
    String pass = request->getParam("pass", true)->value();
    String city = request->getParam("city", true)->value();
    String api = request->getParam("api", true)->value();

    // 打印接收到的参数
    Serial.println(ssid);
    Serial.println(pass);

    // 保存WiFi信息到JSON文件
    DynamicJsonDocument doc(1024);
    doc["ssid"] = ssid;
    doc["pass"] = pass;
    doc["city"] = city;
    doc["api"] = api;
    fs::File file = SPIFFS.open(ssidFile, "w");  // 打开文件进行写入
    if (file) {
        serializeJson(doc, file);  // 将JSON内容写入文件
        file.close();  // 关闭文件
    }

    // 更新全局变量
    cityname = city;
    weatherapi = api;

    // 开始连接WiFi
    WiFi.begin(ssid.c_str(), pass.c_str());
    // 发送HTML响应,告知用户正在连接
    // 发送带UTF-8编码声明的HTML响应

request->send(200, "text/html; charset=UTF-8",
""
""
""
" "
" 状态"
""
""
"

请返回使用在线功能,如果能正常获取则配置成功!

"
""
""
);}
);

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
          {
    // 检查SPIFFS文件系统中是否存在index.html文件
    if (SPIFFS.exists("/index.html")) {
        fs::File file = SPIFFS.open("/index.html", "r");  // 打开index.html文件
        if (file) {
            size_t fileSize = file.size();  // 获取文件大小
            String fileContent;

            // 逐字节读取文件内容
            while (file.available()) {
                fileContent += (char)file.read();
            }
            file.close();  // 关闭文件

            // 返回HTML内容
            request->send(200, "text/html", fileContent);
            return;
        }
    }
    // 如果文件不存在,返回404错误
    request->send(404, "text/plain", "File Not Found"); });
server.on("/control.html", HTTP_GET, [](AsyncWebServerRequest *request)
          {
    // 检查SPIFFS文件系统中是否存在index.html文件
    if (SPIFFS.exists("/control.html")) {
        fs::File file = SPIFFS.open("/control.html", "r");  // 打开index.html文件
        if (file) {
            size_t fileSize = file.size();  // 获取文件大小
            String fileContent;

            // 逐字节读取文件内容
            while (file.available()) {
                fileContent += (char)file.read();
            }
            file.close();  // 关闭文件

            // 返回HTML内容
            request->send(200, "text/html", fileContent);
            return;
        }
    }
    // 如果文件不存在,返回404错误
    request->send(404, "text/plain", "File Not Found"); });
server.on("/engine.html", HTTP_GET, [](AsyncWebServerRequest *request)
          {
    // 检查SPIFFS文件系统中是否存在index.html文件
    if (SPIFFS.exists("/engine.html")) {
        fs::File file = SPIFFS.open("/engine.html", "r");  // 打开index.html文件
        if (file) {
            size_t fileSize = file.size();  // 获取文件大小
            String fileContent;

            // 逐字节读取文件内容
            while (file.available()) {
                fileContent += (char)file.read();
            }
            file.close();  // 关闭文件

            // 返回HTML内容
            request->send(200, "text/html", fileContent);//发送文件内容
            return;
        }
    }
    // 如果文件不存在,返回404错误
    request->send(404, "text/plain", "File Not Found"); });
server.on("/setting.html", HTTP_GET, [](AsyncWebServerRequest *request)
          {
    // 检查SPIFFS文件系统中是否存在index.html文件
    if (SPIFFS.exists("/setting.html")) {
        fs::File file = SPIFFS.open("/setting.html", "r");  // 打开index.html文件
        if (file) {
            size_t fileSize = file.size();  // 获取文件大小
            String fileContent;

            // 逐字节读取文件内容
            while (file.available()) {
                fileContent += (char)file.read();
            }
            file.close();  // 关闭文件

            // 返回HTML内容
            request->send(200, "text/html", fileContent);
            return;
        }
    }
    // 如果文件不存在,返回404错误
    request->send(404, "text/plain", "File Not Found"); });
// 启动服务器
server.begin();

};
void loadWiFiConfig()
{
// 初始化SPIFFS文件系统(存储WiFi配置等信息)
if (SPIFFS.begin()) // 成功挂载文件系统
{
// 尝试打开存储WiFi配置的JSON文件(需提前创建)
fs::File file = SPIFFS.open(ssidFile, "r"); // "r"表示只读模式
if (file) // 文件存在且可访问
{
// 创建动态JSON文档(容量需根据实际配置数据调整)
DynamicJsonDocument doc(1024); // 建议至少1024字节存储配置参数

    // 反序列化JSON数据(将文件内容解析为JSON对象)
    DeserializationError error = deserializeJson(doc, file);
    
    if (!error) // JSON解析成功
    {
        // 从JSON对象中提取配置参数
        String ssid = doc["ssid"];     // WiFi名称字段
        String pass = doc["pass"];     // WiFi密码字段
        String city = doc["city"];     // 城市代码字段
        String api = doc["api"];       // 天气API密钥字段
        
        // 将配置参数赋给全局变量
        cityname = city;        // 存储城市代码
        weatherapi = api;       // 存储API密钥
        
        // 使用存储的凭证尝试连接WiFi
        WiFi.begin(ssid.c_str(), pass.c_str()); // 转换为C风格字符串
        
        // 设置5秒连接超时(5000ms)
        unsigned long startAttemptTime = millis();
        while (WiFi.status() != WL_CONNECTED && 
              millis() - startAttemptTime < 5000)
        {
            delay(500); // 等待连接,每0.5秒检测一次
        }

        // 连接状态检测
        if (WiFi.status() != WL_CONNECTED) 
        {
            Serial.println("WiFi connection failed, starting captive portal...");
            handleWiFiConfig(); // 启动强制配置门户(如AP模式)
        }
        else 
        {
            Serial.println("WiFi connected");
            timeClient.begin(); // 初始化NTP时间客户端
        }
    }
    file.close(); // 关闭文件释放资源
}

}

}
void fetchWeather()
{ // 天气捕捉
// 天气数据初始化模块(首次运行或需要更新时触发)
if (initweather == false)
{
// 检测WiFi连接状态(确保网络可用性)
if (WiFi.status() == WL_CONNECTED)
{
WiFiClient client; // 创建TCP客户端
HTTPClient http; // 初始化HTTP客户端

    // 构建带参数的API请求URL(包含动态参数)
    String apiUrl = weatherAPI + weatherapi + "&location=" + cityname + "&language=zh-Hans&unit=c&start=0&days=1";
    
    // 发起HTTPS连接(注意:实际需确认weatherAPI是否支持SSL)
    if (http.begin(client, apiUrl)) 
    {
        int httpCode = http.GET();  // 发送GET请求
        
        // 成功接收响应(httpCode 200表示成功)
        if (httpCode > 0) 
        {
            String payload = http.getString();  // 获取完整响应数据
            
            // 调试输出原始JSON数据(建议在开发阶段开启)
            Serial.println("JSON Response:");
            Serial.println(payload);
            
            // 创建JSON文档并解析数据
            DynamicJsonDocument doc(2048);  // 建议扩大至2048字节防止数据截断
            DeserializationError error = deserializeJson(doc, payload);
            
            if (!error) 
            {
                // 提取天气数据(注意字段路径需与API响应结构匹配)
                String temperature2 = doc["results"][0]["daily"][0]["high"];     // 最高温度
                String humidity2 = doc["results"][0]["daily"][0]["humidity"];     // 湿度值
                String weathe2r = doc["results"][0]["daily"][0]["text_day"];     // 天气描述(变量名疑似拼写错误)
                
                // 更新全局天气变量
                temperature = temperature2;
                humidity = humidity2;
                weather = weathe2r;
                initweather = true;  // 标记已完成初始化
                
                // 调试输出解析结果
                Serial.print("Data received: ");
                Serial.println(temperature);
                Serial.println(humidity);
                Serial.println(weather);
            }
            else 
            {
                Serial.println("JSON解析失败: " + String(error.c_str()));
            }
        }
        else 
        {
            Serial.printf("HTTP请求失败,错误代码: %d,详情: %s\n", 
                        httpCode, http.errorToString(httpCode).c_str());
        }
        http.end();  // 必须释放资源
    }
    else 
    {
        Serial.println("服务器连接失败,请检查API地址");
    }
}

}
if (weather == "小雨" || weather == "大雨" || weather == "暴雨" || weather == "雨")//识别天气
{
do
{
u8g2.setFont(u8g2_font_ncenB08_tr);//配置字体
u8g2.drawXBMP(0, 0, 64, 64, rain);//展示图片
u8g2.drawStr(64, 20, "Temp");//显示温度
String temperatureString = String(temperature) + " C";//拼接字符串
u8g2.drawStr(64, 30, temperatureString.c_str());//屏幕显示
u8g2.drawStr(64, 50, "Humidity");//内容同上不再注释
String humidityString = String(humidity) + " %";
u8g2.drawStr(64, 60, humidityString.c_str());
} while (u8g2.nextPage());
}
else if (weather == "晴")
{
do
{
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(64, 20, "Temp");
u8g2.drawXBMP(0, 0, 64, 64, sun);
String temperatureString = String(temperature) + " %";
u8g2.drawStr(64, 30, temperatureString.c_str());
u8g2.drawStr(64, 50, "Humidity");
String humidityString = String(humidity) + " %";
u8g2.drawStr(64, 60, humidityString.c_str());
} while (u8g2.nextPage());
}
else
{
do
{
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawXBMP(0, 0, 64, 64, cloud);
u8g2.drawStr(64, 20, "Temp");
String temperatureString = String(temperature) + " C";
u8g2.drawStr(64, 30, temperatureString.c_str());
u8g2.drawStr(64, 50, "Humidity");
String humidityString = String(humidity) + " %";
u8g2.drawStr(64, 60, humidityString.c_str());
} while (u8g2.nextPage());
}
}

void front()
{
servo2.write(140); //舵机2旋转至140度
servo3.write(40); //舵机旋转至40度
delay(100);//延时100s
servo1.write(40); //内容同上
servo4.write(140);
delay(100);
servo2.write(90);
servo3.write(90);
delay(100);
servo1.write(90);
servo4.write(90);
delay(100);
servo1.write(140);
servo4.write(40);
delay(100);
servo2.write(40);
servo3.write(140);
delay(100);
servo1.write(90);
servo4.write(90);
delay(100);
servo2.write(90);
servo3.write(90);

}
void back()
{
servo3.write(140);
servo2.write(40);
delay(100);
servo4.write(40);
servo1.write(140);
delay(100);
servo3.write(90);
servo2.write(90);
delay(100);
servo4.write(90);
servo1.write(90);
delay(100);
servo4.write(140);
servo1.write(40);
delay(100);
servo3.write(40);
servo2.write(140);
delay(100);
servo4.write(90);
servo1.write(90);
delay(100);
servo3.write(90);
servo2.write(90);

}
void right()
{
int num = 0;
while (num < 3)//调用一次执行3次
{
servo1.write(100);
servo4.write(100);
delay(100);
servo3.write(60);
servo2.write(60);
delay(100);
servo1.write(140);
servo4.write(140);
delay(100);
servo3.write(40);
servo2.write(40);
delay(100);
servo3.write(90);
servo2.write(90);
servo1.write(90);
servo4.write(90);
delay(100);
servo1.write(80);
servo4.write(80);
delay(100);
servo3.write(120);
servo2.write(120);
delay(100);
servo1.write(90);
servo4.write(90);
delay(100);
servo3.write(140);
servo2.write(140);
delay(100);
servo3.write(90);
servo2.write(90);

    num++;
}

}
void left()
{

int num = 0;
while (num &lt; 3)
{
    servo1.write(80); 
    servo4.write(80); 
    delay(100);
    servo3.write(120); 
    servo2.write(120); 
    delay(100);
    servo1.write(40); 
    servo4.write(40); 
    delay(100);
    servo3.write(140); 
    servo2.write(140); 
    delay(100);
    servo3.write(90); 
    servo2.write(90); 
    servo1.write(90); 
    servo4.write(90); 
    delay(100);
    servo1.write(100); 
    servo4.write(100); 
    delay(100);
    servo3.write(60); 
    servo2.write(60); 
    delay(100);
    servo1.write(90); 
    servo4.write(90); 
    delay(100);
    servo3.write(40); 
    servo2.write(40); 
    delay(100);
    servo3.write(90); 
    servo2.write(90); 

    num++;
}

}

void sitdown()
{
servo2.write(140);
servo4.write(40);
delay(3000);
servo2.write(90);
servo4.write(90);

}
void lie()
{
servo1.write(180);
servo3.write(0);
servo2.write(0);
servo4.write(180);
delay(3000);
servo1.write(90);
servo3.write(90);
servo2.write(90);
servo4.write(90);

}

void toplefthand()
{
int num = 0;
while (num < 3)
{
servo3.write(0);
delay(100);
servo3.write(30);
delay(100);

    num++;
}
servo3.write(90);

}
void toprighthand()
{

int num = 0;
while (num &lt; 3)
{
    servo1.write(180); 
    delay(100);
    servo1.write(150);
    delay(100);

    num++;
}
servo1.write(90);

}
void dosleep()
{
servo1.write(0);
servo3.write(180);
servo2.write(180);
servo4.write(0);
}

// 对 ADC 数据多次采样并计算平均值
float getAverageAdcVoltage()
{
long totalAdcValue = 0;

// 多次采样
for (int i = 0; i &lt; numSamples; i++)
{
    totalAdcValue += analogRead(A0); // 读取 ADC 数据
    delay(10);                       // 每次采样间隔 10ms
}

// 计算平均 ADC 值
float averageAdcValue = totalAdcValue / (float)numSamples;

// 将 ADC 值转换为电压
return (averageAdcValue / 1023.0) * 1.0; // ESP8266 的参考电压为 1.0V

}

// 计算电池电量百分比的函数
int mapBatteryPercentage(float voltage)
{
if (voltage <= minVoltage)
return 0; // 小于等于最小电压时,电量为 0%
if (voltage >= maxVoltage)
return 100; // 大于等于最大电压时,电量为 100%

// 根据线性比例计算电量百分比
return (int)((voltage - minVoltage) / (maxVoltage - minVoltage) * 100);

}
void serialListen(){

// 读取完整字符串(直到换行符)
String receivedString = Serial.readStringUntil('\n');
// 去掉可能的回车符或空格
receivedString.trim();
// 处理接收到的字符串
Serial.print("Received: ");
Serial.println(receivedString);
if(receivedString=="front"){//当接收到“front”时
    front();//执行前进
};
if(receivedString=="back"){
    back();
};
if(receivedString=="toplefthand"){
    toplefthand();
};
if(receivedString=="toprighthand"){
    toprighthand();
};
if(receivedString=="left"){
    left();
};
if(receivedString=="right"){
    right();
};
if(receivedString=="sitdown"){
    sitdown();
};
if(receivedString=="lie"){
    lie();
};
if(receivedString=="dosleep"){
    dosleep();
};
if(receivedString=="kaixin"){
    emojiState=0;
};
if(receivedString=="shengqi"){
    emojiState=1;
};
if(receivedString=="nanshou"){
    emojiState=5;
};
if(receivedString=="haoqi"){
    emojiState=3;
};
if(receivedString=="xihuan"){
    emojiState=4;
};
if(receivedString=="cuowu"){
    emojiState=2;
};
if(receivedString=="yun"){
    emojiState=6;
};
if(receivedString=="shijian"){
    emojiState=8;
};
if(receivedString=="tianqi"){
    emojiState=7;
};
if(receivedString=="logo"){
    emojiState=9;
};
      

}
void setup()
{
u8g2.begin();
u8g2.setDisplayRotation(U8G2_R2);
// OLED 显示初始化与按钮中断配置
u8g2.firstPage(); // 启动U8g2页面缓冲绘制
do {
// 设置显示字体(14像素高度,适合128x64屏幕)
u8g2.setFont(u8g2_font_ncenB14_tr);

// 绘制LOGO位图(居中显示计算)
// 参数说明:X坐标0(左对齐),Y坐标(64/2 -22/2)=21(垂直居中)
// 位图尺寸:宽度128px,高度22px,数据源为logo数组
u8g2.drawXBMP(0, (64 / 2 - 22 / 2), 128, 22, logo);

} while (u8g2.nextPage()); // 循环刷新直至完成全帧绘制

// 按钮1配置(通常对应GPIO2)
// 硬件要求:按钮接地触发,内置上拉保持高电平
pinMode(BUTTON_PIN, INPUT_PULLUP);
// 配置下降沿中断(物理按下时产生低电平)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonPress, FALLING);

// 按钮2配置(通常对应GPIO15)
// 硬件要求:需外接上拉电阻,按钮接3.3V触发
pinMode(BUTTON_PIN2, INPUT); // 无内部上拉模式
// 配置上升沿中断(物理按下时产生高电平)
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN2), handleButtonPress, RISING);
//启用SPIFFS文件系统
SPIFFS.begin();
servo1.attach(engine1, 500, 2500); // 配置舵机PWM,500µs=0度,2500µs=180度
servo2.attach(engine2, 500, 2500);
servo3.attach(engine3, 500, 2500);
servo4.attach(engine4, 500, 2500);
servo1.write(90);//舵机旋转到90度
servo3.write(90);
servo2.write(90);
servo4.write(90);
// 初始化串口
Serial.begin(115200);
// 设置WiFi为热点模式
WiFi.softAP(ssid, password);
Serial.println("热点已启动");
// 访问的IP地址是 ESP8266 的默认IP:192.168.4.1
Serial.print("访问地址: ");
Serial.print(WiFi.softAPIP());
// 加载WiFi配置
loadWiFiConfig();
if (WiFi.status() != WL_CONNECTED)//当WIFI未连接时
{
Serial.println("Starting captive portal...");//串口输出
handleWiFiConfig();//加载WIFI配置
}
else
{
handleWiFiConfig();//加载WIFI配置
Serial.println("WiFi connected");
timeClient.begin();//NTP服务初始化
timeClient.update(); // 获取初始时间
}
delay(5000);
u8g2.clearDisplay();//清屏
do
{
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawXBMP(0, 0, 64, 64, ipip);
u8g2.drawStr(64, 10, "WIFI_AP");
u8g2.drawStr(64, 27, "EDA-Robot");
u8g2.drawStr(64, 43, "192.168.4.1");
u8g2.drawStr(64, 60, "WIFI CTRL");
} while (u8g2.nextPage());
delay(5000);
u8g2.clearDisplay();
}

void loop()
{
if (Serial.available() > 0) {
serialListen();
}
// 对 ADC 数据多次采样并求平均
float adcVoltage = getAverageAdcVoltage();

// 将采样的 ADC 电压转换为实际电池电压
batteryVoltage = adcVoltage * voltageDividerRatio; // 计算电池电压

// 根据电池电压计算电量百分比
batteryPercentage = mapBatteryPercentage(batteryVoltage);

if (buttonPressed)//按键1按下时
{
    buttonPressed = false; // 清除按键标志
    front();
}
if (buttonPressed2)
{
    buttonPressed2 = false; // 清除按键标志
    back();
}
if (emojiState != prevEmojiState)
{
    u8g2.clearDisplay();         // 状态变化时清屏
    prevEmojiState = emojiState; // 更新状态
}
if (freestate)
{
    delay(3000);
    actionstate = random(0, 10);
}
// 可以使用switch优化效率
switch (actionstate)
{
case 0 /* constant-expression */:
    /* code */
    break;
case 1:
    front(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 2:
    left(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 3:
    right(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 4:
    back(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 5:
    toplefthand(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 6:
    toprighthand(); // 执行一次舵机动作
    actionstate = 0;
    break;

case 10:
    dosleep(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 7:
    lie(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 8:
    sitdown(); // 执行一次舵机动作
    actionstate = 0;
    break;
case 9:
    emojiState = random(0, 7); // 执行一次舵机动作
    actionstate = 0;
    break;
default:
    break;
}

switch (emojiState)
{
case 0: // 首页
    u8g2.setFont(u8g2_font_ncenB14_tr);
    do
    {

        u8g2.drawXBMP(0, 0, 128, 64, hi);
    } while (u8g2.nextPage());

    break;
case 1: // 第二页

    u8g2.setFont(u8g2_font_ncenB14_tr);
    do
    {

        u8g2.drawXBMP(0, 0, 128, 64, angry);
    } while (u8g2.nextPage());

    break;
case 2: // 第三页
    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, 0, 128, 64, error);
    } while (u8g2.nextPage());

    break;
case 3: // 第四页
    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, 0, 128, 64, dowhat);
    } while (u8g2.nextPage());

    break;
case 4: // 第四页

    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, 0, 128, 64, love);
    } while (u8g2.nextPage());
    break;
case 5: 

    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, 0, 128, 64, sick);
    } while (u8g2.nextPage());
    break;
case 6: 
    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, 0, 128, 64, yun);
    } while (u8g2.nextPage());

    break;
case 7: 
    if (WiFi.status() != WL_CONNECTED)
    {
        do
        {
            u8g2.setFont(u8g2_font_ncenB08_tr);
            u8g2.drawXBMP(0, 0, 64, 64, wifi);
            u8g2.drawStr(64, 20, "IP:");
            u8g2.drawStr(64, 40, "192.168.4.1");
            u8g2.drawStr(64, 60, "Need NET");
        } while (u8g2.nextPage());
    }
    else
    {
        fetchWeather();
    }
    break;

    break;
case 8:
    if (WiFi.status() != WL_CONNECTED)
    {
        do
        {
            u8g2.setFont(u8g2_font_ncenB08_tr);
            u8g2.drawXBMP(0, 0, 64, 64, wifi);
            u8g2.drawStr(64, 20, "IP:");
            u8g2.drawStr(64, 40, "192.168.4.1");
            u8g2.drawStr(64, 60, "Need NET");
        } while (u8g2.nextPage());
    }
    else
    {
        do
        {
            timeClient.update(); // 更新时间
            u8g2.setFont(u8g2_font_ncenB14_tr);
            timeClient.update();
            u8g2.drawXBMP(0, 0, 64, 64, timeimage);
            // 获取当前时间
            // 显示时间到 OLED
            int currentHour = timeClient.getHours();
            int currentMinute = timeClient.getMinutes();
            String timeToDisplay = String(currentHour) + ":" + String(currentMinute);
            u8g2.drawStr(64, 30, "TIME");
            u8g2.setCursor(64, 50);
            u8g2.print(timeToDisplay);

        } while (u8g2.nextPage());
    }
    break;
case 9: 

    do
    {
        u8g2.setFont(u8g2_font_ncenB14_tr);
        u8g2.drawXBMP(0, (64 / 2 - 22 / 2), 128, 22, logo);
    } while (u8g2.nextPage());
    break;
default:
    // 添加默认 case 来处理其他情况
    break;
}

}

注意事项

1.推荐使用180度舵机,自带限位器,无需校准
2.V1.3外壳适配光固化打印,固定PCB螺丝M2x2,固定外壳螺丝M1.4x3
3.OLED显示屏为0.96寸SSD1306或SSD1315驱动
4.电池为2节14500锂电池,单节电压3.7-4.2v, 电池盒为2节串联14500电池盒
5.预留开关和外部充电接口,外部充电模块应当为2串锂电池充电模块,满电电压8.1-8.4V,均衡接口需飞线连接到两节电池中间。
6.切勿强行掰动舵机,避免电机损坏,也可购买金属齿轮的MG90舵机
7.14500单节电池容量建议达到1000mAh-1200mAh左右,确保放电能力达到1C,最高输出电流最好达到1200mA,以达到所需电流值。

实物图

电路板

93134b5079039b6787b68ec49a663584_720.jpg
焊接完成

b19345b5aecfbfd276a49b124a40b44e_720.jpg
组装完成并上电

820a7280c598da688a863f0b0840828c.jpg

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

附件

序号文件名称下载次数
1
机器狗结项作业.mp4
0
2
3D外壳.zip
0
3
EDA-Robot(180度舵机版)_V1.1 (1).zip
0
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航