微信小程序+ESP8266+Arduino 开发智能插座,有本地OTA功能

1.Arduino程序

c 复制代码
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <EEPROM.h>
#include <ESP8266HTTPUpdateServer.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>
#include <ArduinoJson.h>          // 必须安装

// ---------- 引脚定义 ----------
#define RELAY_PIN   5   // D1
#define LED_PIN     2   // D4, 低电平亮

// ---------- 网络参数 ----------
#define AP_SSID     "SmartPlug"
#define AP_IP       IPAddress(192, 168, 4, 1)
#define AP_GATEWAY  IPAddress(192, 168, 4, 1)
#define AP_SUBNET   IPAddress(255, 255, 255, 0)

#define TCP_PORT    8080
#define UDP_PORT    8080
#define HTTP_PORT   8266

// ---------- EEPROM 地址 ----------
#define EEPROM_SIZE         512
#define WIFI_FLAG_ADDR      0
#define SSID_ADDR           1
#define PASS_ADDR           33
#define TIMER_BASE_ADDR     100
#define WEEKDAYS            7
#define MAX_SLOTS_PER_DAY   4

// ---------- 全局对象 ----------
WiFiServer tcpServer(TCP_PORT);
WiFiUDP udp;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 8 * 3600, 60000);
ESP8266WebServer httpServer(HTTP_PORT);
ESP8266HTTPUpdateServer httpUpdater;   // 只保留本地 OTA 上传

// ---------- 定时结构 ----------
struct TimeSlot {
  bool enabled;
  char timeOn[6];
  char timeOff[6];
};

struct DaySchedule {
  int slotCount;
  TimeSlot slots[MAX_SLOTS_PER_DAY];
};

DaySchedule weekSchedule[WEEKDAYS];

// ---------- 其他全局变量 ----------
bool relayState = false;
unsigned long lastPrintTime = 0;
unsigned long lastTimeCheck = 0;

// ---------- 函数声明 ----------
void setupMode();
bool connectWiFi(const char* ssid, const char* pass);
void saveWiFiCredentials(const char* ssid, const char* pass);
void readWiFiCredentials(char* ssid, char* pass);
void clearWiFiCredentials();
void handleTCPCommand(const String& cmd, WiFiClient &client);
void handleUDPPacket();
void checkTimers();
void setRelay(bool state);
void setupHTTPRoutes();
void loadTimersFromEEPROM();
void saveTimersToEEPROM();

void setup() {
  Serial.begin(115200);
  Serial.println(F("\n\n--- Smart Plug Start ---"));

  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);

  EEPROM.begin(EEPROM_SIZE);
  loadTimersFromEEPROM();

  setupHTTPRoutes();

  uint8_t wifiFlag = EEPROM.read(WIFI_FLAG_ADDR);
  if (wifiFlag == 0xAA) {
    char ssid[33] = {0};
    char pass[65] = {0};
    readWiFiCredentials(ssid, pass);
    if (connectWiFi(ssid, pass)) {
      setupMode();
      return;
    } else {
      clearWiFiCredentials();
    }
  }

  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(AP_IP, AP_GATEWAY, AP_SUBNET);
  WiFi.softAP(AP_SSID);
  setupMode();
}

void loop() {
  // TCP 客户端处理(保留但简化)
  WiFiClient client = tcpServer.available();
  if (client) {
    String cmd = "";
    bool gotCommand = false;
    while (client.connected() && !gotCommand) {
      if (client.available()) {
        char c = client.read();
        cmd += c;
        if (c == '\n') {
          cmd.trim();
          handleTCPCommand(cmd, client);
          gotCommand = true;
        }
      }
      yield();
    }
    client.stop();
  }

  handleUDPPacket();
  httpServer.handleClient();

  if (WiFi.status() == WL_CONNECTED) {
    checkTimers();
  }

  static unsigned long lastBlink = 0;
  if (millis() - lastBlink >= 500) {
    lastBlink = millis();
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }

  if (millis() - lastPrintTime >= 1000) {
    lastPrintTime = millis();
    Serial.printf("[%s] Relay=%s", WiFi.getMode() == WIFI_AP ? "AP" : "STA", relayState ? "ON" : "OFF");
    if (WiFi.status() == WL_CONNECTED) Serial.printf(" IP:%s", WiFi.localIP().toString().c_str());
    Serial.println();
  }
  delay(1);
}

// ---------- EEPROM 读写 ----------
void loadTimersFromEEPROM() {
  uint8_t flag = EEPROM.read(TIMER_BASE_ADDR);
  if (flag != 0xA5) {
    for (int d = 0; d < WEEKDAYS; d++) {
      weekSchedule[d].slotCount = 0;
      for (int s = 0; s < MAX_SLOTS_PER_DAY; s++) {
        weekSchedule[d].slots[s].enabled = false;
        strcpy(weekSchedule[d].slots[s].timeOn, "08:00");
        strcpy(weekSchedule[d].slots[s].timeOff, "22:00");
      }
    }
    saveTimersToEEPROM();
    return;
  }

  int addr = TIMER_BASE_ADDR + 1;
  for (int d = 0; d < WEEKDAYS; d++) {
    weekSchedule[d].slotCount = EEPROM.read(addr++);
    if (weekSchedule[d].slotCount > MAX_SLOTS_PER_DAY) weekSchedule[d].slotCount = MAX_SLOTS_PER_DAY;
    for (int s = 0; s < MAX_SLOTS_PER_DAY; s++) {
      weekSchedule[d].slots[s].enabled = EEPROM.read(addr++);
      for (int j = 0; j < 5; j++) {
        weekSchedule[d].slots[s].timeOn[j] = EEPROM.read(addr++);
      }
      weekSchedule[d].slots[s].timeOn[5] = '\0';
      for (int j = 0; j < 5; j++) {
        weekSchedule[d].slots[s].timeOff[j] = EEPROM.read(addr++);
      }
      weekSchedule[d].slots[s].timeOff[5] = '\0';
    }
  }
}

void saveTimersToEEPROM() {
  EEPROM.write(TIMER_BASE_ADDR, 0xA5);
  int addr = TIMER_BASE_ADDR + 1;
  for (int d = 0; d < WEEKDAYS; d++) {
    EEPROM.write(addr++, weekSchedule[d].slotCount);
    for (int s = 0; s < MAX_SLOTS_PER_DAY; s++) {
      EEPROM.write(addr++, weekSchedule[d].slots[s].enabled);
      for (int j = 0; j < 5; j++) {
        EEPROM.write(addr++, weekSchedule[d].slots[s].timeOn[j]);
      }
      for (int j = 0; j < 5; j++) {
        EEPROM.write(addr++, weekSchedule[d].slots[s].timeOff[j]);
      }
    }
  }
  EEPROM.commit();
}

// ---------- 定时检查 ----------
void checkTimers() {
  if (millis() - lastTimeCheck < 10000) return;
  lastTimeCheck = millis();
  if (!timeClient.update()) return;

  String now = timeClient.getFormattedTime().substring(0, 5);
  int day = timeClient.getDay();
  if (day < 0 || day > 6) return;

  DaySchedule &today = weekSchedule[day];
  static String lastActionTime = "";
  if (now == lastActionTime) return;

  for (int s = 0; s < today.slotCount; s++) {
    TimeSlot &slot = today.slots[s];
    if (!slot.enabled) continue;
    if (now == String(slot.timeOn) && !relayState) {
      setRelay(true);
      lastActionTime = now;
      Serial.printf("Timer ON (day %d, slot %d)\n", day, s);
      break;
    }
    if (now == String(slot.timeOff) && relayState) {
      setRelay(false);
      lastActionTime = now;
      Serial.printf("Timer OFF (day %d, slot %d)\n", day, s);
      break;
    }
  }
}

// ---------- HTTP 路由 ----------
void setupHTTPRoutes() {
  httpServer.on("/status", []() {
    httpServer.send(200, "text/plain", "DEVICE_OK");
  });

  httpServer.on("/state", []() {
    String json = "{\"relay\":\"" + String(relayState ? "on" : "off") + "\"}";
    httpServer.send(200, "application/json", json);
  });

  httpServer.on("/on", []() {
    setRelay(true);
    httpServer.send(200, "application/json", "{\"result\":\"ok\",\"state\":\"on\"}");
  });

  httpServer.on("/off", []() {
    setRelay(false);
    httpServer.send(200, "application/json", "{\"result\":\"ok\",\"state\":\"off\"}");
  });

  // 配网
  httpServer.on("/config", HTTP_POST, []() {
    if (httpServer.hasArg("plain")) {
      String body = httpServer.arg("plain");
      int ssidStart = body.indexOf("\"ssid\":\"") + 8;
      int ssidEnd = body.indexOf("\"", ssidStart);
      int pwdStart = body.indexOf("\"pwd\":\"") + 7;
      int pwdEnd = body.indexOf("\"", pwdStart);
      if (ssidStart >= 8 && pwdStart >= 7) {
        String ssid = body.substring(ssidStart, ssidEnd);
        String pwd = body.substring(pwdStart, pwdEnd);
        saveWiFiCredentials(ssid.c_str(), pwd.c_str());
        httpServer.send(200, "application/json", "{\"result\":\"ok\"}");
        delay(1000);
        ESP.restart();
        return;
      }
    }
    httpServer.send(400, "application/json", "{\"result\":\"error\"}");
  });

  // 获取定时
  httpServer.on("/timers", HTTP_GET, []() {
    const char* dayNames[] = {"周日","周一","周二","周三","周四","周五","周六"};
    String json = "[";
    for (int d = 0; d < WEEKDAYS; d++) {
      if (d > 0) json += ",";
      json += "{";
      json += "\"day\":" + String(d);
      json += ",\"label\":\"" + String(dayNames[d]) + "\"";
      json += ",\"slots\":[";
      for (int s = 0; s < weekSchedule[d].slotCount; s++) {
        if (s > 0) json += ",";
        json += "{";
        json += "\"enabled\":" + String(weekSchedule[d].slots[s].enabled ? "true" : "false");
        json += ",\"on\":\"" + String(weekSchedule[d].slots[s].timeOn) + "\"";
        json += ",\"off\":\"" + String(weekSchedule[d].slots[s].timeOff) + "\"";
        json += "}";
      }
      json += "]";
      json += "}";
    }
    json += "]";
    httpServer.send(200, "application/json", json);
  });

  // 保存定时
  httpServer.on("/timers", HTTP_POST, []() {
    if (httpServer.hasArg("plain")) {
      String body = httpServer.arg("plain");
      Serial.printf("Timers POST body: %s\n", body.c_str());

      DynamicJsonDocument doc(4096);
      DeserializationError error = deserializeJson(doc, body);
      if (error) {
        Serial.printf("JSON error: %s\n", error.c_str());
        httpServer.send(400, "application/json", "{\"result\":\"error\"}");
        return;
      }

      JsonArray arr;
      if (doc.is<JsonArray>()) {
        arr = doc.as<JsonArray>();
      } else if (doc.containsKey("timers")) {
        arr = doc["timers"].as<JsonArray>();
      } else {
        httpServer.send(400, "application/json", "{\"result\":\"error\"}");
        return;
      }

      for (int d = 0; d < WEEKDAYS; d++) {
        weekSchedule[d].slotCount = 0;
        for (int s = 0; s < MAX_SLOTS_PER_DAY; s++) {
          weekSchedule[d].slots[s].enabled = false;
          strcpy(weekSchedule[d].slots[s].timeOn, "08:00");
          strcpy(weekSchedule[d].slots[s].timeOff, "22:00");
        }
      }

      for (JsonObject dayObj : arr) {
        int day = dayObj["day"] | -1;
        if (day < 0 || day >= WEEKDAYS) continue;
        JsonArray slotsArr = dayObj["slots"];
        if (!slotsArr) continue;
        int s = 0;
        for (JsonObject slotObj : slotsArr) {
          if (s >= MAX_SLOTS_PER_DAY) break;
          weekSchedule[day].slots[s].enabled = slotObj["enabled"] | false;
          String on = slotObj["on"] | "08:00";
          String off = slotObj["off"] | "22:00";
          if (on.length() > 5) on = on.substring(0, 5);
          if (off.length() > 5) off = off.substring(0, 5);
          strcpy(weekSchedule[day].slots[s].timeOn, on.c_str());
          strcpy(weekSchedule[day].slots[s].timeOff, off.c_str());
          s++;
        }
        weekSchedule[day].slotCount = s;
      }

      saveTimersToEEPROM();
      httpServer.send(200, "application/json", "{\"result\":\"ok\"}");
      Serial.println(F("Timers saved (week multi-slot)"));
    } else {
      httpServer.send(400, "application/json", "{\"result\":\"error\"}");
    }
  });
}

// ---------- 其他函数 ----------
void setupMode() {
  tcpServer.begin();
  udp.begin(UDP_PORT);

  // OTA 升级页面(用于手机上传 .bin 文件)
  httpUpdater.setup(&httpServer, "/update");

  if (WiFi.getMode() == WIFI_STA) {
    timeClient.begin();
    timeClient.update();
  }

  httpServer.begin();
  Serial.printf("HTTP server started on port %d\n", HTTP_PORT);
}

bool connectWiFi(const char* ssid, const char* pass) {
  WiFi.begin(ssid, pass);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED && tries < 40) {
    delay(500);
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    tries++;
  }
  digitalWrite(LED_PIN, HIGH);
  return WiFi.status() == WL_CONNECTED;
}

void saveWiFiCredentials(const char* ssid, const char* pass) {
  EEPROM.write(WIFI_FLAG_ADDR, 0xAA);
  for (int i = 0; i < 32; i++) EEPROM.write(SSID_ADDR + i, i < strlen(ssid) ? ssid[i] : 0);
  for (int i = 0; i < 64; i++) EEPROM.write(PASS_ADDR + i, i < strlen(pass) ? pass[i] : 0);
  EEPROM.commit();
}

void readWiFiCredentials(char* ssid, char* pass) {
  for (int i = 0; i < 32; i++) ssid[i] = EEPROM.read(SSID_ADDR + i);
  ssid[32] = 0;
  for (int i = 0; i < 64; i++) pass[i] = EEPROM.read(PASS_ADDR + i);
  pass[64] = 0;
}

void clearWiFiCredentials() {
  EEPROM.write(WIFI_FLAG_ADDR, 0x00);
  EEPROM.commit();
}

void handleTCPCommand(const String& cmd, WiFiClient &client) {
  // 不再使用 TCP 控制,简单响应即可
  client.println("use HTTP");
}

void handleUDPPacket() {
  int packetSize = udp.parsePacket();
  if (packetSize) {
    char incoming[255];
    int len = udp.read(incoming, 255);
    if (len > 0) {
      incoming[len] = 0;
      String msg(incoming);
      msg.trim();
      if (msg == "SCAN") {
        udp.beginPacket(udp.remoteIP(), udp.remotePort());
        udp.write("DEVICE_OK");
        udp.endPacket();
      }
    }
  }
}

void setRelay(bool state) {
  relayState = state;
  digitalWrite(RELAY_PIN, state ? LOW : HIGH);
}

2.微信小程序

cpp 复制代码
拖入的目录结构:
================================
[文件夹] C:\Users\Administrator\WeChatProjects\miniprogram-1
Folder PATH listing
Volume serial number is 000000B8 94EB:D5C5
C:\USERS\ADMINISTRATOR\WECHATPROJECTS\MINIPROGRAM-1
©¦   .gitignore
©¦   app.js
©¦   app.json
©¦   app.miniapp.json
©¦   app.wxss
©¦   project.config.json
©¦   project.miniapp.json
©¦   project.private.config.json
©¦   sitemap.json
©¦   
©À©¤©¤©¤i18n
©¦       base.json
©¦       
©À©¤©¤©¤miniapp
©¦   ©À©¤©¤©¤android
©¦   ©¦   ©¦   i18nInfo.json
©¦   ©¦   ©¦   
©¦   ©¦   ©¸©¤©¤©¤nativeResources
©¦   ©¦       ©À©¤©¤©¤app
©¦   ©¦       ©À©¤©¤©¤assets
©¦   ©¦       ©¸©¤©¤©¤res
©¦   ©¦           ©¸©¤©¤©¤raw
©¦   ©¸©¤©¤©¤ios
©¦           i18nInfo.json
©¦           
©À©¤©¤©¤pages
©¦   ©À©¤©¤©¤config
©¦   ©¦       config.js
©¦   ©¦       config.json
©¦   ©¦       config.wxml
©¦   ©¦       config.wxss
©¦   ©¦       
©¦   ©À©¤©¤©¤index
©¦   ©¦       index.js
©¦   ©¦       index.json
©¦   ©¦       index.wxml
©¦   ©¦       index.wxss
©¦   ©¦       
©¦   ©À©¤©¤©¤logs
©¦   ©¦       logs.js
©¦   ©¦       logs.json
©¦   ©¦       logs.wxml
©¦   ©¦       logs.wxss
©¦   ©¦       
©¦   ©À©¤©¤©¤ota
©¦   ©¦       ota.js
©¦   ©¦       ota.json
©¦   ©¦       ota.wxml
©¦   ©¦       ota.wxss
©¦   ©¦       
©¦   ©¸©¤©¤©¤timer
©¦           timer.js
©¦           timer.json
©¦           timer.wxml
©¦           timer.wxss
©¦           
©¸©¤©¤©¤utils
        util.js
        

================================
================================ 
时间:周五 2026/05/15 20:40:18.71 
================================ 
 
 
================================ 
【文件夹】C:\Users\Administrator\WeChatProjects\miniprogram-1 
================================ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.gitignore ===== 
# Windows
[Dd]esktop.ini
Thumbs.db
$RECYCLE.BIN/

# macOS
.DS_Store
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes

# Node.js
node_modules/
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\app.js ===== 

App({
  onLaunch() {
    console.log('智能插座小程序启动')
  },
  globalData: {
    deviceIP: "",
    deviceState: "off",
    isConnected: false
  }
}) 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\app.json ===== 
{
  "pages": [
    "pages/index/index",
    "pages/config/config",
    "pages/timer/timer",
    "pages/ota/ota"
  ],
  "window": {
    "navigationBarTitleText": "智能插座",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black"
  },
  "sitemapLocation": "sitemap.json"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\app.miniapp.json ===== 
{
	"adapteByMiniprogram": {
		"userName": "gh_f6160cf4db7c"
	}
}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\app.wxss ===== 
/**app.wxss**/
.container {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: 200rpx 0;
  box-sizing: border-box;
} 
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\project.config.json ===== 
{
  "compileType": "miniprogram",
  "libVersion": "trial",
  "packOptions": {
    "ignore": [],
    "include": []
  },
  "setting": {
    "coverView": true,
    "es6": true,
    "postcss": true,
    "minified": true,
    "enhance": true,
    "showShadowRootInWxmlPanel": true,
    "packNpmRelationList": [],
    "babelSetting": {
      "ignore": [],
      "disablePlugins": [],
      "outputPath": ""
    },
    "compileWorklet": false,
    "uglifyFileName": false,
    "uploadWithSourceMap": true,
    "packNpmManually": false,
    "minifyWXSS": true,
    "minifyWXML": true,
    "localPlugins": false,
    "condition": true,
    "swc": false,
    "disableSWC": true,
    "disableUseStrict": false,
    "useCompilerPlugins": false
  },
  "condition": {},
  "editorSetting": {
    "tabIndent": "auto",
    "tabSize": 2
  },
  "appid": "wx89c4a5fd311848ec",
  "simulatorPluginLibVersion": {
    "wxext14566970e7e9f62": "2.27.3"
  },
  "projectArchitecture": "multiPlatform"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\project.miniapp.json ===== 
{
  "miniVersion": "v2",
  "name": "%name%",
  "version": "0.0.1",
  "versionCode": 100,
  "i18nFilePath": "i18n",
  "mini-ohos": {
    "sdkVersion": "0.5.1"
  },
  "mini-android": {
    "resourcePath": "miniapp/android/nativeResources",
    "sdkVersion": "1.6.24",
    "toolkitVersion": "0.11.0",
    "useExtendedSdk": {
      "media": false,
      "bluetooth": false,
      "network": false,
      "scanner": false,
      "xweb": false
    },
    "icons": {
      "hdpi": "",
      "xhdpi": "",
      "xxhdpi": "",
      "xxxhdpi": ""
    },
    "splashscreen": {
      "hdpi": "",
      "xhdpi": "",
      "xxhdpi": ""
    },
    "enableVConsole": "open",
    "privacy": {
      "enable": true
    }
  },
  "mini-ios": {
    "sdkVersion": "1.7.0",
    "toolkitVersion": "0.0.9",
    "useExtendedSdk": {
      "WeAppOpenFuns": true,
      "WeAppNetwork": false,
      "WeAppBluetooth": false,
      "WeAppMedia": false,
      "WeAppLBS": false,
      "WeAppOthers": false
    },
    "enableVConsole": "open",
    "icons": {
      "mainIcon120": "",
      "mainIcon180": "",
      "spotlightIcon80": "",
      "spotlightIcon120": "",
      "settingsIcon58": "",
      "settingsIcon87": "",
      "notificationIcon40": "",
      "notificationIcon60": "",
      "appStore1024": ""
    },
    "splashScreen": {
      "customImage": ""
    },
    "privacy": {
      "enable": false
    },
    "enableOpenUrlNavigate": true
  }
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\project.private.config.json ===== 
{
  "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
  "projectname": "SmartPlug",
  "setting": {
    "compileHotReLoad": true,
    "urlCheck": false,
    "coverView": true,
    "lazyloadPlaceholderEnable": false,
    "skylineRenderEnable": false,
    "preloadBackgroundData": false,
    "autoAudits": false,
    "useApiHook": true,
    "showShadowRootInWxmlPanel": true,
    "useStaticServer": false,
    "useLanDebug": false,
    "showES6CompileOption": false,
    "bigPackageSizeSupport": false,
    "checkInvalidKey": true,
    "ignoreDevUnusedFiles": true
  },
  "libVersion": "3.16.0"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\sitemap.json ===== 
{
  "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
  "rules": [{
    "action": "allow",
    "page": "*"
  }]
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\config ===== 
[core]
	bare = false
	repositoryformatversion = 0
	filemode = false
	symlinks = false
	ignorecase = true
	logallrefupdates = true
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\description ===== 
Unnamed repository; edit this file 'description' to name the repository.
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\HEAD ===== 
ref: refs/heads/master
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\index ===== 
DIRC      j[    j[             ¤           Ÿúp¾ȥ†HI˜C¤&ŠªZ½ 
.gitignore        jj  jթ             ¤            Œπ̭>>⺐鼐¦£|C± app.js    jj  jY·             ¤          *¿
û\ GüHy p򞨺y݁" app.json  jj  jj           ¤           †Ƽœ㛽౱责aG2큠app.wxss  jҡ    j!             ¤          Š▿°Ÿ'1RA_q`眄`F犣 pages/config/config.js    jӨ    j¼             ¤           .D³ㆁ󛆅[
¯饟WT¬_µ pages/config/config.json  jҹ    jթ             ¤          ­Љμ¿":;‰Τ¾C¡@ pages/config/config.wxml  jҒ    jթ             ¤          P°œӭV򜴉֣пZx¥Aª4 pages/config/config.wxss  jj  jXǠ            ¤          
-¬̤ϒ"ӌ•ĮA8™jﻬ£² pages/index/index.js      jj  j׉             ¤           .ʴ-k$„õ٦ÿ{56a¯´ը pages/index/index.json    jj  j⃠            ¤          Աnں ¹s¢[멉ü峉ٴ pages/index/index.wxml    jj  jթ             ¤          †¡Nꠛ욋A䱂ݝ䟇h6 pages/index/index.wxss    jj  jj           ¤          1
öªū۲¢|ߵī!&±[ pages/logs/logs.js        jj  jj           ¤           µ[Z%Au򅲚H›Žõ¶öå¦ pages/logs/logs.json      jj  jj           ¤           ŏþyǘĢ,"ü	
ö
ϩW] pages/logs/logs.wxml      jj  jj           ¤           ÷3ù١ܠŸj먳㜤8vΝ$ pages/logs/logs.wxss      jց    jւ             ¤          À̟ꎹÿrùǏኮܯ¹ˆ% pages/ota/ota.js  jց    jפ             ¤           0™£
:¾Ϥv"´¼B
?= pages/ota/ota.json        jց    jւ             ¤          +õy1©9†m"C %ú;)跑 pages/ota/ota.wxml        jց    jւ             ¤          
%¬f,ݛ¦ÿS›Ҵ􇽈pm pages/ota/ota.wxss        jց    jւ             ¤          šõq„1šŽ>>l?-뷌£ pages/timer/timer.js      jց    jי             ¤           0úޅg9+rYDXk%򘾪¢i pages/timer/timer.json    jց    jւ             ¤          Pܕ1ʇjªú˜6"Zùӭ> pages/timer/timer.wxml    jց    jւ             ¤          Kݱ鍽2%6弸hû򡠋Ŧ| pages/timer/timer.wxss    jj  jj           ¤          }:u üɷ底sԸ퇄ül<" project.config.json       jj  jj           ¤          ‰ۤT퐋컚øƕ¶¡'楞 project.private.config.json       jj  jթ             ¤           ç²²m7›è븻™փ¬-7N0 sitemap.json      jj  jj           ¤          ̶KŽ&<<›U¢>>ψJ„߽ 
utils/util.js     I\ƻ	[¶·˜`Xzö 3ý 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\hooks\README.sample ===== 
#!/bin/sh
#
# Place appropriately named executable hook scripts into this directory
# to intercept various actions that git takes.  See `git help hooks` for
# more information.
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\info\exclude ===== 
# File patterns to ignore; see `git help ignore` for more information.
# Lines that start with '#' are comments.
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\logs\HEAD ===== 
0000000000000000000000000000000000000000 01b7670ed2b0d10a8554e11069295d8bdfce01b1 unknown <unknown@example.com> 1778735894 +0800	commit (initial): Initial Commit
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\logs\refs\heads\master ===== 
0000000000000000000000000000000000000000 01b7670ed2b0d10a8554e11069295d8bdfce01b1 unknown <unknown@example.com> 1778735894 +0800	commit (initial): Initial Commit
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\01\b7670ed2b0d10a8554e11069295d8bdfce01b1 ===== 
x•Á
!E[ûo„š"O"hէ<õIҨ18ԧ'я´>>8熖J޵•̤0b8i層b$+)F㌪﹉---"BO‚¶~o+lõQ۫¼~Sy.|­œG֢=N茬%J)wÿŭ枩뷰ҭ9ú 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\06\c6fc9ce35b7d02e031b1e8f4636147321dedc1 ===== 
x́ƒ Ю=Ŭº!A±馸Q§A €Q۴"ÿ~~F[¯¡=oc*„v?Rb¬kڑ>>¬ȡ„o° ͋--ЋqJ6"‚U§„ɢQ¡^n(☉;	£·۪j£,͎SƵF---1V~o)Ӵ򺧠„Ԉ\cޑ¯aPƐ›%<„ˆ᠑gڟ<ѧr?ȋ
͏š?žՁL 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\10\fa70bec8e58648499843a426118aaa5a7f04bd ===== 
xͱ°`瀾C¡®ƶppն(HS)yڨ"W'T񟻜¾>>ᎍ¨'|µž¥ɉ8ŸÀ٥З¯ˆ£0ΰִ"UAhřüPnϛ])6U½䌳4±ݭ/9
leDĻ€7¸4YŽ󨣢˜g
ؑ}翕Ko¢񝨁??ƒxRp„֢ž??ø2> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\19\99a30d3abecfa47693b4bc420e0d90edb9ba3d ===== 
xKʉOR0±`¨楒PPʋ,ˌO,ɌϳJ,
ɬɉ
I­(Q²RPz:{דݛžö¶?ߵ\‰---<< $.0 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\25\ac662cdd9ba6ff539bd2340501f4073dc8706d ===== 
xeъƒ †w􎇆.hCš=¦k‚©¸ŢwŸfÁƮηýÿƉh轔õ΢זXÀs)µܪৎP͈¸уeЫ‹*t°--E
‚¥, '/ýV®4A)ytd6
ԱKԄƒ͐‚'۹---£iD8D72 ۢ,=0i‰Om}9Fùlϭ?I›öγ¥??¾~ nR¹ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\27\b2b26d379bc328ebf87b99d643ac2d374e0730 ===== 
xKʉOR0´4e¨撐PJI-NV²RPzںùɮ¾gsּ›־d÷¶糚ž͞򴉬'û>k\ÿ~Oϋõ۟ö7½hh*x¶p±BFIIA±•¾~JjYjN~AjQ±^yjfEfž^a¡^r~®~nf^fAQ~zQb.H'~'Zž_"­_œY'š›X ---Q'›£¤rEQiNj1ЙѠ7ù‰ɥ™ùy w%椤---ƒ
ӓA‚ZJ@eµ±\µ ҀR 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\33\616dd5346895721e6c3696823938bfc0296567 ===== 
x+)JMU06e040031Q(-Ɍы*f(󾴎mõ쐅2>>٦ž簪'¸ÿ /X 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\33\f9d9e1dca09f6aeb68b3e39c243876ce5d240f ===== 
x5ŽÁNİD9÷+|„CPY!!e¿ƭ܄'G‰)‹оû6N--Ƴof]ಾ񔰒üL ‰8&󰶏GºžBք¿=lB·.??Z5{XU>÷|[<<Š`%¬=¤Ս􋝠ġЯ@4:6ڇ}ǚ9;ӢᲗ2ڌn搸ö&ʆu4ý½`3·&--0Cܝ¢fº{ |<7܈õQŽs#ûû½t΃ɓ¯ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\37\9661355a30f70ad4697fdb4944e85bf3aff335 ===== 
x+)JMU045`040031QȌKI­Ћ*fXsF弥)---y¦Y稱3뽯œśД䧱œ*эViùú@rٿjS³ă﷜ՀQU^'›ð1¦rŋ¢_krþyú™󦖴UŅmý^-¸ýFŠ۱‰¡ӝ٧÷ٳ̠荂 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\3a\7500fcc9f7e5fa95738dd478edc7c4fc6c3c94 ===== 
xmSˮۀ칟a蜋š´pz
P €Q(z V´D„Z.öQŵüdGR{!‡œYnɒ®ַ7<<U¦¤5ĸ?û¼ʚҤ¬Ԗڬ}♊g´ŽD'ڛꥫ񑰑IZ±šj-6	ýü•º¢‡j€"rꅝzOº~듲?!oŽͨ>́#Ϋ---NªҎ{ªŽ¢n@<<4xRꚩ򆪩žDüƒ.^[ށFž---%o¦}B†dpC·‚‹§J伡΅#V'ƒ'qǡ¦>©³J䤸ü|ճ¬¬(r㻔b_Ӵ=°;璪¦ýዼ·Gh"řiX *ȷ¹<<p&VLBݭA`>,ڻ4ŏ<Ÿw񭦎³(෻Ӎ"芒€‹®SdL)/#þݡOOý"ƒûᾭbƒ˩Mw8ׇq)±˷˼"ºB†d¼??"9ýI1¸Ȃ1".랗wꖾ뫵횕𽜵!^Œ‹m¦ߨxº:ýEú
Ѡ
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\44\b3e34601f39b86855b0aafe925df5754ac5fb5 ===== 
xKʉOR01c¨楒PPʋ,ˌO,ɌϳJ,
ɬɉ
I­(Q²RP
ϴ˼ٚû|^®Z ކý 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\45\18d8c621b8cfae8a70add49482bbef09f118ba ===== 
x+)JMU065`040031QЋϬɌϋ/JeøU°6ϙ΋Ԅ>>VEճ셪J,(Ћ*f蹿̛杲vMxi3a٢g¡(*򳘶·þŽY྇£rAÁ'¹>>*ﲊNARS^Q\̀v쏜ǑµL7¾ø'œ认ûö ‰($¦§3쬸aۮ'°0ܨ¦eýʭ7w™ϭ‡šQP"Ÿ•š\¢---œŸ-----™tЊ<<R†?'¿?ý5µ¸÷Jśㇾ䘀¬„)/(ʬK,IEіy[%䭠÷›ߑ?Ž]ݶIýٓyP[Š3KRsA~¯¾iS®ù샚¯Tϼ漆לݠ⚒'̜bㄜ<<&S‹䲌¦5YZ쿠™š z)•$ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\50\1adc1531ca876a03aafa9d9836945af9d32d3e ===== 
xKʉOR05¶`°)ˌ-WHΉ,.¶UJΏ+ǏK-R²㥒P@'+ɬɉU²{ºnֳ雞ںùź}Ϸ®³чր¡)³$5bд'Ԋ; Χsvx9}˻=³l􁢠@™ə©E
¹ù)©¶J%™¹©J
I™y)ə‰y逑┒ÿ¼0ض°d烴낌T²<<®·<<­­Er Ў}ˆ*ˆaN'ơ­›)rxZ	.‡(&ީI¥%%ùy*I, †TbY*Ⱥ"%
'ʂ`Еe榖U*Áb:©$Oɮɾ¹O׎€Ĩ>Č`¸@Ë ŸS>> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\76\4bc2ce26ab9b55a21cbb069dcf084a8418dffd ===== 
xu'1kð
;ûWܐˆM'@鐨>>"'%Yҥ„€嚍\¬SOc򟻒ҔK'[wtª;SÃ㼮ӠO𥬖??OЈ⏳Œ@lJګC½¹®ûҞܔڠɚŸ󂮡qӴrH¯œ'8---F¬øاmݢ󔁎3¬C!•􊡛D²
/a<<ș„j2JĥϛöB˓W°qºV¶ߦż:›`‡‡cGˆ,!ý<<ʮY--f:ü]-ÿ 
2[²-¯¸[졅•K¨擑㴭ש¡Χc©g³Ÿ¿‡〟ů˜栍
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\85\cf1bfe79c718c4222c94fc090af60dcf29575d ===== 
xMAƒ E>>öS¶
šš®ҽꔐ‡҈©‹´·oþ¼?ӑ렭o'q朜諥µĹª„劉Ŀ艻/ُ陵ƒÿxƒ°½Q22>0UˆŽ\?Á²ޟn--,Ɲšۉ„›dÁX|m߅吷m6¸±²☙q	0£y
¸
®)Ր̵ ¦$š}¥Է¥?ߟP՗>>N5 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\85\f6aac5ab16db728fa27cdf75c4ab2126b1105b ===== 
xMOÁŽ ݳ_1·B²¡&f/MܓG&õf<кvkZP˜FöߝHW%dfx÷xT<<`>ûùʳ蜓??ـ0Pہ<ž‡̴֣ιG4¤ʷ3%Ě4(oȔ'HNlw|¿¹8>>rf/Մӟt@Z²$iߚy¹ꆩ$癸¼ٚf1W¦ౠG¥{s'ŒÀⷲ‹b4xû¤<X¤_胳½¡Mۣ´x~£
R1ۿ"¦K2ý©ˆ񟄸M£JCj£඄ˆ/W¨ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\86\a14eeaa0dbec1a0b41e43142dd1de4df076836 ===== 
xmQݮƒ ޵Oq']Ө¬n꓀ '!4xl͚¾{8˜ởߡڲ¨ʢ픛ƒT᠞\(犈ι>>¬Ÿ0ڿIgÉoµu-¼Ux᳒†ŒBɑ[(򼺶٣;¡B-"ݠɉ¬¾EBg·m‰Y̓Š	ՊšzaP¸HOT~R3‡я‡²,£֌---™0>>FÁ---ۖn£Bv˜u\8⨗ˬ֛´=l俉z6D;mº©Œ'œŽÁˡˆݭ±|A›ik྇ʿúþv	||]ם¯˜ 𦉠†^&ܪ+³¨v{‡b>c/';2<<7 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\89\db2454ed500becfb5af8c6d5b6a10227e6e59e ===== 
xuRÁnӀ圯°rFNR½•¦¨•ˆbQΫ{jo³ޱ׫¤)ª¨I¢P¥
---„^@(_Cl§'~1I¥&ˆƒ¥ռ緯͌-жݸ²`"G񐳔
%£py2̺ƒ쳳??ֱösvG?¾ÿzú<?¼G罬{´[ɠ˜œµǧ?1B
۠hӁ¹Žs;BiL˜YYý*鿍߈gڢ{¾&/¿Œ.ޥ­#"¤ýOɑٵ.D="™Ρz򡷅ח:Œ--ŠE꠰™
ໜš;;Թ(\rr⩖䤼ӈ"*N퍝ù:
›yþi]² 򼖀"®ˆ؛ hͥGH>,¢SXC]

d.!ZŐK+±⃓›­:Xµɡ1[l¯)H¢"˜>
Ԫd¶Ƚl1]‰Fµ¦Ҫ ÿLj
ƒ¹÷˜S󔆒-3ͦTX¬q9v¹Ž怸‚启!ιŽ|lX>s±QEԫ򱮠*L‚˜MA¿[ši(濢L--Á¦Υ"ùU<<´2™烫¼ܦ^
"1,¾V†¨??{]֙ஃhκ㞄e¨?'dӽO‹˓狣}?¿ඦѴüo›·J梡aÿ?Π
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\8c\cfc0cced03bb1ee2ba90e93c90a6a37c4312b1 ===== 
xKʉOR043`p,(Ш撐ȏ󉬍KΐД q'󳊳sRõr򓵔Ÿ͜õ¢yﳾIOw-º¡ÿùŠúŸNXÿ´k
º&Py­Hš™˜㒘'h5#%µ,39ճÀJAI	¤BA"\'X'
̏KƒŠg;秥¥&---¤¦X)¤%攧‚L媕 ד;™ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\93\77a3c7317b48fb8d3d1f2dfc089202126db990 ===== 
x+)JMU045`040031Q(ɌM-ҋ*f˜%򵰅ސxV߮œ·t_›û,FS"Ÿǰ끜kº¥vQ¤D¶꧉:­E™(ªʫrs¤žjϢ^õk)Q?/뚡©*.f¸[øҗֈլ©͏Œߟ>𾚖 ¬2=3 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\9a\14f571841f31139a8ebb6cede3102deb374ca3 ===== 
x}'Mkۀ†{6ø?L|Zƒj ԕ¤¦---öИ⻙H#e‰²+¤±|n顴֐úqI.
䘣󧪓ŸљФ¢ٙwžygöK·ϟŸ¤ζºª`
¤?ýng¤T§ݎ@¦I'°úpvlŽ0^<L⸷Ž湈onJX¢s>"wƒ´>>º¤°ߪЁi">x͒괭ŠQ†¤MMu9A˜3†ױ*¾ȣ2-Ń:+=E­՚Ǥ 6؀¨`Ot)TL25)¾­" fǑsণ§bj2TŠ˯¿oo~.?_ܞýYœ荀°¥|`Ş肜ª'&µü³DJſ.'֨	ǻ£=---"ɬ䂟G,k1•ھ ³¬ƦId|ÿ£¬3+WSø‡wŸȻ>>
°5[/ۡ_z"1ߛ½ݷQCµ±
ɏü~󉥯¥eiXPŒ;r')ϯeù±µ-µ\¾噭6<8
ŕ·嗫¿7?---gˏ_Ÿ~ݵžwMP܄"¶zj
¶ú•NU OŸű¨6¯ۙ÷ÿ•Ǭ¢ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\ac\cc24cfd294d30c95c4ae4138996aeffb6ca3b2 ===== 
x•V[kGŴ_¼¢b%F!#>>`z±ÁNú(£լ¼d5#fGqƒ#p ±c㤉)M/Рм$m§±sqúg¼k驿¡猬®ֲ]ƒ¤e挙󝎙ׂQ#cʣ¹‚‡ŠЖ‹\"
¦&[->>02G̞!¤N­|‚gvݷٌ\
XVQ¯øaUpΜŪ①df݄Ϋªϳ𼡥‰öDk‡߽ŽWnAŽNq¾ÿBк]H.°{뱳'Q÷i´½'¢ýЉ™š‚²t}˜uP p€Œछ䦍¬dBŽ=t"·‡‡rҫPC粀õeY3ԩd U|W{~ˆ¶Va=dTº‹Sš֌|ÀÀ¤
䀴uߣ¶ƞ®·
†xr܀„̮\D	Yú։Œ‚ ¡²---‰򕀴•{+_ûŸù{wMŽ㘅⃴° õ>oÀB½-©򱱼\."Nš8½J‚;\ɀ׋Ss󂽆^·§ANͧ eRS¶*ø---,і¶da\ú4›DŒ>ƒ??*‰®$©MˆŝɚB±֫°'DzỶDö⇯£;/ّϿE[ÿ¾„°[€Ԥׅk[ƻO㛛л秾꺴û򰝦üퟴ?ۨýM¿>>A¬©髳թof?7ʂ'@L东
ڵڹ˜Bjf䦄·̌¿eػ!*‚l³„SŒE‡k܈=_Aœ	>\ϥ<<褃͌gŒ;ń񚇌L¤'|¢yL,¶]d°´e0þeҧ.5띝Uvi2ퟵ?À˜칶>ɤ!¤%¤ª‰򄹅‚ZÀùꤗi󥌹mS՚ހɔ[r2¯$t㉑¬.RYu怮Á
›·ƒ H8["---}®&&¥¤7lL'ؓ™9†ƒk󾿥ւu.º¿𮫰ѭ¨û,ꮇ<<û񏯠aVE³I­۬‰Kƒ FݫiEh<<5+ڜ© 鶩õþڍºw򣦐㜰--WǠ6֫˜顧Ž‹=--õ8 걗fŽ™ø0`2Ž[̉"›#p\𤕡Äˍ{KҗIwYW啮e I•>¥³g`&}ô"Bڶ"򈕩t€uضP#-V´µݻõȂi;ž҇FաsI"ŒŽx˔V1#g클~%޼>zûG⋙ž½4§y¯Œ.[ÀŒU±·:£E2Dߑc2²0ϫⰒ¡ =f?‹‚ݗ±.}`¯cýŸ!`ؔݝþϏBõ¼󠅷迅‹ÿm˜ûqޜ‚φkʿ?ܞÿYÿ÷¿û¿>,﹍/x‹ӫ~^£©-ƒ
-µ୘--À¥›üŒ&H`¼`ž¿ɤùҨ-鯡$³"狡-Á';>ҩüOٛ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\b0\9cd36d56f29cf4c9d6a3d07f115a78a541aa34 ===== 
xu˪1E>>䟮d퐦Š)Ηأw8--񨐚򯵣S²鶠ݫ١š??$--R(x [舘§'÷>>£ĀǾ|Ցµҷ0˜thP"´$ƒ9$	¥㫭U45N•
Yª¶fŽ\Z뗋r,----֐Qʷ2Dž‡ºöގ­ɣ཯†¿J瞩%ޢuңC¡Šõt[
>·v/•Ͽ°õp'껆H	筚ž?TOg㠍
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\b1\6edaba2002b91e73a25beb2909fce5f309d9b4 ===== 
xSMKðö쯨¹줳¨²DvRP𜶩tɨ27‚""
~†sxEœ 2anúgl;OþӴݚف½š÷y򼤚I4ifzj,³`Yҍ•R脳ahe\'b[1Źl÷v>>Ή¹ݾϤ=¬2><J™ʊtR#A3D좆Á6`°"^󃾝w[ζΏ§žI‹¿œݻĈT®̢Cժ¢󄣨3˜ۚ¡€œ†ɭp¹‡ާµs|7‚š>]n ®pᐖS§81P췧>>³mŸ>¤£𯔟·÷򝻌₸„>>hфŽ}‰٥®̫G "±e---ydժ`*ù¨𐯳­ĘÁ'†pŽ©EP¨Zúú‚ˆHl³eP´PAµ6ûkŒǠœ6ܗ¿®Lڧf˜1OxIʇ᫨}ݳ(¸‹úǫöM'ˆƹ%ʥ%]‡"Əs.xk᝝ȍŒ€³FHWV-N<aݖ5þ‹+OVQ?¥8¾Ywj-ۭFݧhɓ¥չ!??½e?뛸	[
֟uuvh 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\b5\5b5a25411975f285b21a489b8ef5b6f6c325a6 ===== 
xKʉOR06`¨撐P*-ΌKwΏ-ȏKͫ)V²R 	ײՂ ⟋› 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\b9\80f83d872460a15732d9397fa9b5d9ba379d87 ===== 
x+)JMU04µ`01 
似´̴†¯M>>ʫ˜5l÷¯l'b·䥉0˜Q™---'ZÁ`>-Ѵʠ;ו̺۞./¢?¯ÿl
'ωO/fx´t廓*­ݽퟞ?Á꟧¸fA¤󋒙ΰ=󒓫uÁ	]
û{ˊKõ&@dK2sS‹&---/>nX?ז^÷Ǥ&¡ܝ n‹Aˠ
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\bf\85fb5ca047fc4879a070f21ee8ba79dd011594 ===== 
xmQMKÀõܟ֫4{񘏂‚`n⡓Lšt'Š޼‹ DГA𔨿ƴý֍¶ج٥öͼ÷f֏зZ­퍋†㰔ǠَsLAºB†0¨v¶µ(#>>ձ
х喻*€†>>??±d¬O??k€bɻ"憠ls剓€C8›>峫ɴö¡ȟjҕ춏:±Œ†>>˜ ²5›Qù­M·¼G漁›觔\§ù눚-뻘úVp--	៨†հς¬ꏋ]ž6eµW͓²"´sHAu
֔7¥,f¡L¡™iPK商¤Xº٣ûw~S|\£Ѭr?½{ž}¾̇_ūµµK†ÿ™>T¢Ǎ쉈˧f1˜_‰ʷӇձ衱³󧝋Әþ 	ÿº㠍
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\ca\742d6b2484f5e019a6ff7b353661c2efb4d528 ===== 
xKʉOR01c¨撐Pʋ,ˌO,ɌϳJ,
ɬɉ
I­(Q²RPz6s׋潏ú'=ݵ\‰<< i򠍊------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\cc\06cdcce9092a85a0c82d20dfbd017675752e90 ===== 
x+)JMU041b040031QȯIԋ*f8sŸù_忢Ÿ2ǽz­>>³~g‡*ŠŠü<ə‹y­ö_R6y˞'>މowRS^'›ðµҐi¥Œe[՟֚/¶ODQS\̠º&M箬eÿƒg_2aeü®{¢  >>:w 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\cc\df03ea0e79ff72f91cc74fe14aaedcafb98825 ===== 
xu'ÁJÀ†=úcN)†A°*„¶‡žo½l"5c6l&V(9׃Š žZÁ‚|苘ڇpӤ
º§ݝٿþùf‡¾¶·e1?D A mp(ꁠ7굃8T׫ !އ¦.7`upO@ޖTGh¯K¨6½p-ڷ։ £K5<e#"'屠‹m´¸Z~>,ng˹G2H
¸‚ø̧Ģ8¬§q?;ŵZ¶񨂚©W!oqJš㈙gSߩKW
¤O­´n!Gl›Ӱl¥­þ圙gŒcšZS˯RO¥4󻹸@Ҿ(¨duG܅*Kº¹ө]ݬ
øÀ---֍U 愣Or3ù~üz{VUU°)rJ#¶뻒RF숓tS†>>šXE $Y܄M÷œ²劵§•ى旋<<>>rTadY‚審Ŋ ¦iy£Ub=Ι¯­°ؤL_--¯³²䦯Ⱥ¿"nü ûГ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\d0\c9cebc150ebf943a1b3b890418cee4be43a140 ===== 
xKʉOR01²d°)ˌ-WHΉ,.¶UJΏ+ǏK-R²㥒P@'+ɬɉU²ϴ˼ٚû|}Vl
"재߾bÿ¼g}K_¬۷tIû󦵏›v>ЇֺI+؞̼‚ҒSA¡ '195#?'%µȖ	dۓ	½ϗoЈötє‚*JʌKk²U*N-IÁd >K*Lԇ¸¿뛞/h„jx@8v³a΅\y~Q
ª°'JKJ򳔀F•$ ]Y'XT✟-----™®¤PRYj<<TP"™›XT©ü¤'<%>>§{ž.0Ĕ   ™ݠ
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\dd\71e94d3d322536e53cf868fbf2a1e04bc5667c ===== 
xU۪1Dûȿ򬰡K
¢]Ժ-c+4iȿǗRڗat436°
y>½G11Ý'񞢪ᵊ麎ýᾟ,ժ
}£†ù܌M¼Š2֨Áa̝ތ^©Js#ÁTµ#Ám°<•̍Òp€>.Eh¹©¨r4"d*‹򅘻´¿Q
Rþ=û͝¿u<<嬱+ˢ¼i8µ
y8 b·ü­ӻŽŒ‰ܧ½l½ꤎg
‡鍍=¬ĪzJ8i8ÿŒõ---Bj] 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\e2\963fb09f923152415f7160e7dc046046e70a63 ===== 
x}S[kAö9пpº٠X' %"Ĕ!>ȋ:ݝ¦K·;ˬ¤҅[	Iբ⋗„汭bAmÿK馲䟰̎.%÷i.g¾󝎮8lҩ•̭"¹¾ ⹐‡:ϓK±2©S½µ°ˆ 9h\ø¾m倓'㽗Œ¶ѮH.Ť•OE¥RZթbúLlپ竈¦·ƨ԰¨ ¶c쒧A!HLQ Za}¹ö?˜¨ùb"ˆ‹ \™>>i׵¤£D8T 3)%	ˆJJ­ªĞ}YVÀޞ,cѴ1@󉡯±f•_ a‡栾<û6¼x¶>>ᩧÿöüú򳿿,žƫ---¹4Ž"> §¢Á]µ"q ²mĄ˜^Bq•a:̧2U¾ˆÀ凾ˮ𴢼9¼þ޳c֝aIJݺX
N„-)¥SøEŒ¬š쇶`"S"hµX®0s›
ٞNÀ„ž􌴦""--̵©‰--¨B bYœú>΋ú^ƈ¯d>>FZé'8 㢇ٔ6¥N¤7㞨™¹[„‡Á寏²4™C
‡յ?†'ס‹Oڔ_²¹c!͵BõN±ö¸PΫ·[2ӀK⊃
´5¾殏IMnªラЂ·G?¯㰕񨩾꽳ۯ˜&
¿7s՞¡¬!??鍈•7є¹d׮cˆ¹=± HBƒ󛷭!猫:弟®ᅞkW:c߂"/ó.þвɄ‰š•ӪůŸœø°ýu??=*𺰬78ø1sb~꿒ª~ü‚ğ֞nܠ
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\e2\a5a9eec924fd3bc6ddd0fc3c9130abf9c80a9a ===== 
x+)JMU041c040031QȉO/֋*fhý¶ꨪ±ۅý‹jY­¨¶Q UI~Ö訕GɒO­›¤<f÷}ݶ?2dE啹9­祿U---8¢¤3叧׷޳šᱨŠŠ‹Œ޼xgÁü¬י›ϑ±(;<< 0Á>½ 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\f5\793102a91c39866d9343001025fa3b29e8b791 ===== 
xKʉOR0²´d°)ˌ-WHΉ,.¶UJΏ+ǏK-R²㥒P@'+ɬɉU²{:{דݛüCŸö¶?ߵ܆¤SuybQž'ݣYޯ藀(}±¿ýùŠ·?ힿlښ瓶"i[---TZR'Ÿ§"™---R'X`<<T\'XT´JI¡¤² Ֆ	l(̭I%@žº¼b>ĥ6úSÀN¢‰ˆה Žz:¡}O---´?ٳ녻ª§]+^¶÷>°¬淨;ŽԂc 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\f5\82ba777a03283dbfa984181a07390d0c583094 ===== 
x+)JMU045a040031QHΏKˌ׋*fx4;üI†AŽ񅉏ﰤ¸=犆W"ŸǠ²ù±㧙m­ќ럪ޏY¿UYyEnÅ"綈򭟢%m݉"q煎芊‹6̹œöiΗ"ז_¨ŒªX긊 S†@ܠ
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\objects\fa\e05e4567392b7259440f586b25f2187e2aa269 ===== 
xKʉOR0±`¨楒PPʋ,ˌO,ɌϳJ,
ɬɉ
I­(Q²RPzºnֳ雞¬۷|ﺥ^®Z %)f 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\.git\refs\heads\master ===== 
01b7670ed2b0d10a8554e11069295d8bdfce01b1
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\i18n\base.json ===== 
{
	"ios": {
		"name": "SmartPlugSam"
	},
	"android": {
		"name": "SmartPlugSam"
	},
	"common": {
		"name": "SmartPlugSam"
	}
}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\miniapp\android\i18nInfo.json ===== 
{"base":{"ios":{"name":"SmartPlugSam"},"android":{"name":"SmartPlugSam"}}}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\miniapp\ios\i18nInfo.json ===== 
{"base":{"ios":{"name":"SmartPlugSam"},"android":{"name":"SmartPlugSam"}}}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\config\config.js ===== 
const app = getApp()
Page({
  data: { 
    ssid: "", 
    pwd: "",
    deviceOnline: false,    // 替代心跳数字,显示是否在线
    configStatus: "正在检测设备..."
  },

  onLoad() {
    this.checkDeviceStatus()
    // 每 3 秒检测一次设备在线状态
    this.intervalId = setInterval(() => {
      this.checkDeviceStatus()
    }, 3000)
  },

  onUnload() {
    if (this.intervalId) clearInterval(this.intervalId)
  },

  // 通过 HTTP 检查设备是否在线
  checkDeviceStatus() {
    const ip = app.globalData.deviceIP || "192.168.4.1"   // AP 模式默认 IP
    wx.request({
      url: `http://${ip}:8266/status`,
      method: "GET",
      timeout: 2000,
      success: (res) => {
        if (res.data === "DEVICE_OK") {
          this.setData({ deviceOnline: true, configStatus: "设备已连接" })
        } else {
          this.setData({ deviceOnline: false, configStatus: "设备响应异常" })
        }
      },
      fail: () => {
        this.setData({ deviceOnline: false, configStatus: "设备离线" })
      }
    })
  },

  setSSID(e) { this.setData({ ssid: e.detail.value }) },
  setPWD(e) { this.setData({ pwd: e.detail.value }) },

  // 开始配网(HTTP POST)
  startConfig() {
    let { ssid, pwd } = this.data
    if (!ssid || !pwd) {
      wx.showToast({ title: '请输入完整信息', icon: 'none' })
      return
    }
    wx.showToast({ title: '正在配网...', icon: 'loading', duration: 5000 })
    // 注意:配网时设备一般在 AP 模式,IP 是 192.168.4.1
    wx.request({
      url: "http://192.168.4.1:8266/config",
      method: "POST",
      data: { ssid, pwd },
      header: { 'content-type': 'application/json' },
      timeout: 5000,
      success: (res) => {
        wx.hideToast()
        if (res.data && res.data.result === "ok") {
          wx.showModal({
            title: '配网成功',
            content: '设备即将重启并连接WiFi,请稍候重新搜索设备。',
            showCancel: false
          })
        } else {
          wx.showToast({ title: '配网失败,请重试', icon: 'none' })
        }
      },
      fail: () => {
        wx.hideToast()
        wx.showToast({ title: '请求失败,请检查设备连接', icon: 'none' })
      }
    })
  }
}) 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\config\config.json ===== 
{
  "navigationBarTitleText": "WiFi配网"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\config\config.wxml ===== 
<view class="container">
  <view class="title">WiFi配网</view>
  <view class="tip">请连接设备热点后配置</view>

  <!-- 显示设备在线状态 -->
  <view class="status-tip">
    <text wx:if="{{deviceOnline}}" style="color:green;">● 设备在线</text>
    <text wx:else style="color:red;">● 设备离线</text>
    <text> {{configStatus}}</text>
  </view>

  <input placeholder="WiFi名称(SSID)" bindinput="setSSID" class="input"/>
  <input placeholder="WiFi密码" bindinput="setPWD" class="input" password/>
  <button bindtap="startConfig" type="primary" class="btn">开始配网</button>
</view> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\config\config.wxss ===== 
.container { padding: 40rpx; }
.title { font-size: 36rpx; text-align: center; margin: 30rpx 0; }
.tip { text-align: center; color: #666; margin-bottom: 40rpx; }
.input { border: 1rpx solid #ddd; padding: 20rpx; margin: 20rpx 0; border-radius: 8rpx; font-size: 28rpx; }
.btn { margin-top: 40rpx; }
.heartbeat { font-size: 36rpx; text-align: center; color: #07c; margin: 20rpx 0; font-weight: bold; }
.status-tip { text-align: center; color: #666; margin-bottom: 20rpx; } 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\index\index.js ===== 
const app = getApp()
Page({
  data: {
    deviceIP: "",
    isConnected: false,
    deviceState: "off",
    inputIP: "",
    scanning: false      // 防止重复点击
  },

  onLoad() {
    const savedIP = wx.getStorageSync('deviceIP') || ""
    this.setData({
      deviceIP: app.globalData.deviceIP || savedIP || "",
      isConnected: app.globalData.isConnected || false,
      deviceState: app.globalData.deviceState || "off",
      inputIP: savedIP
    })
    // 如果有保存的 IP,自动尝试连接一次
    if (savedIP) {
      this.quickConnect(savedIP)
    }
  },

  // 快速连接(不弹 Toast)
  quickConnect(ip) {
    wx.request({
      url: `http://${ip}:8266/status`,
      method: "GET",
      timeout: 1500,
      success: (res) => {
        if (res.data === "DEVICE_OK") {
          this.onDeviceFound(ip)
        }
      },
      fail: () => {}
    })
  },

  // 扫描局域网(主搜索)
  scanNetwork() {
    if (this.data.scanning) return
    this.setData({ scanning: true })
    wx.showToast({ title: "正在扫描局域网...", icon: "loading", duration: 10000 })

    // 获取本机 IP
    wx.getLocalIPAddress({
      success: (res) => {
        const localIP = res.localip  // 如 "192.168.10.100"
        this.scanSubnet(localIP)
      },
      fail: () => {
        // 如果获取 IP 失败,回退到扫描常见网段
        this.scanCommonSubnets()
      }
    })
  },

  // 根据本机 IP 扫描同子网
  scanSubnet(localIP) {
    const parts = localIP.split('.')
    if (parts.length !== 4) {
      this.scanCommonSubnets()
      return
    }
    const prefix = `${parts[0]}.${parts[1]}.${parts[2]}`  // 如 "192.168.10"
    this.doScan(prefix)
  },

  // 扫描多个常见网段(备用)
  scanCommonSubnets() {
    // 常见家庭网段
    const subnets = ['192.168.1', '192.168.0', '192.168.10', '192.168.31']
    // 依次扫描,每个 2 秒超时
    this.doMultiScan(subnets, 0)
  },

  // 依次扫描多个子网(递归)
  doMultiScan(subnets, index) {
    if (index >= subnets.length) {
      this.setData({ scanning: false })
      wx.hideToast()
      wx.showToast({ title: "未找到设备", icon: "none" })
      return
    }
    wx.showToast({ title: `扫描 ${subnets[index]}.x ...`, icon: "loading", duration: 2000 })
    this.doScan(subnets[index], () => {
      // 当前网段未找到,继续下一个
      setTimeout(() => this.doMultiScan(subnets, index + 1), 300)
    })
  },

  // 实际扫描一个子网的所有 IP (1~254)
  doScan(prefix, onComplete) {
    const total = 254
    const concurrency = 20   // 并发数
    let found = false
    let completed = 0
    let canceled = false

    const checkIP = (suffix) => {
      if (found || canceled) return
      const ip = `${prefix}.${suffix}`
      wx.request({
        url: `http://${ip}:8266/status`,
        method: "GET",
        timeout: 800,      // 快速超时
        success: (res) => {
          if (res.data === "DEVICE_OK" && !found) {
            found = true
            canceled = true
            this.onDeviceFound(ip)
            wx.hideToast()
          }
        },
        fail: () => {},
        complete: () => {
          completed++
          if (completed >= total && !found) {
            // 扫描完毕
            if (onComplete) {
              onComplete()
            } else {
              this.setData({ scanning: false })
              wx.hideToast()
              wx.showToast({ title: "未找到设备", icon: "none" })
            }
          }
        }
      })
    }

    // 使用定时器分批并发,避免瞬间请求过多
    let current = 1
    const batch = () => {
      if (found || canceled) return
      for (let i = 0; i < concurrency && current <= total; i++) {
        checkIP(current++)
      }
      if (current <= total) {
        setTimeout(batch, 50)  // 每 50ms 发一批
      }
    }
    batch()
  },

  // 发现设备后的公共处理
  onDeviceFound(ip) {
    app.globalData.deviceIP = ip
    app.globalData.isConnected = true
    wx.setStorageSync('deviceIP', ip)
    this.setData({
      deviceIP: ip,
      isConnected: true,
      inputIP: ip,
      scanning: false
    })
    wx.showToast({ title: `设备在线: ${ip}`, icon: "success", duration: 3000 })
  },

  // ---------- 手动搜索(保留)----------
  onInputIP(e) {
    this.setData({ inputIP: e.detail.value })
  },

  searchByIP() {
    const ip = this.data.inputIP.trim()
    if (!ip) {
      wx.showToast({ title: '请输入设备IP', icon: 'none' })
      return
    }
    wx.showToast({ title: "连接中...", icon: "loading", duration: 3000 })
    wx.request({
      url: `http://${ip}:8266/status`,
      method: "GET",
      timeout: 2000,
      success: (res) => {
        if (res.data === "DEVICE_OK") {
          this.onDeviceFound(ip)
        } else {
          wx.showToast({ title: "设备响应异常", icon: "none" })
        }
      },
      fail: () => {
        wx.showToast({ title: "连接失败", icon: "none" })
      }
    })
  },

  // ---------- 控制命令 ----------
  sendHttpCommand(path, successCallback) {
    if (!this.data.deviceIP) {
      wx.showToast({ title: "请先搜索设备", icon: "none" })
      return
    }
    wx.request({
      url: `http://${this.data.deviceIP}:8266${path}`,
      method: "GET",
      timeout: 2000,
      success: (res) => {
        if (res.data && res.data.result === "ok") {
          if (successCallback) successCallback(res.data.state)
        } else {
          wx.showToast({ title: "设备响应异常", icon: "none" })
        }
      },
      fail: () => {
        wx.showToast({ title: "命令发送失败", icon: "none" })
      }
    })
  },

  sendOn() {
    this.sendHttpCommand("/on", (state) => {
      this.setData({ deviceState: state })
      app.globalData.deviceState = state
      wx.showToast({ title: "已打开", icon: "success" })
    })
  },

  sendOff() {
    this.sendHttpCommand("/off", (state) => {
      this.setData({ deviceState: state })
      app.globalData.deviceState = state
      wx.showToast({ title: "已关闭", icon: "success" })
    })
  },

  goConfig() { wx.navigateTo({ url: '/pages/config/config' }) },
  goTimer() { wx.navigateTo({ url: '/pages/timer/timer' }) },
  goOTA() { wx.navigateTo({ url: '/pages/ota/ota' }) }
}) 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\index\index.json ===== 

{
  "navigationBarTitleText": "智能V2插座"
}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\index\index.wxml ===== 
<view class="container">
  <view class="title">智能插座</view>
  
  <view class="status-box">
    <view class="status-item">
      <text>设备状态:</text>
      <text wx:if="{{!isConnected}}" class="status-offline">未连接</text>
      <text wx:elif="{{deviceState=='on'}}" class="status-on">已开启</text>
      <text wx:else class="status-off">已关闭</text>
    </view>
    <view class="status-item" wx:if="{{deviceIP}}">
      <text>设备 IP:{{deviceIP}}</text>
    </view>
  </view>

  <!-- 一键扫描(自动发现) -->
  <button bindtap="scanNetwork" type="primary" class="btn" loading="{{scanning}}">
    {{scanning ? '扫描中...' : '一键搜索设备'}}
  </button>

  <!-- 手动输入备用 -->
  <view class="ip-box">
    <input 
      placeholder="手动输入IP (高级)" 
      value="{{inputIP}}" 
      bindinput="onInputIP" 
      class="ip-input"
    />
    <button bindtap="searchByIP" size="mini" type="default" class="search-btn">连接</button>
  </view>

  <button bindtap="goConfig" class="btn">WiFi配网</button>

  <view wx:if="{{isConnected}}">
    <button bindtap="sendOn" type="success" class="btn">打开插座</button>
    <button bindtap="sendOff" type="warn" class="btn">关闭插座</button>
    <button bindtap="goTimer" class="btn">定时设置</button>
    <button bindtap="goOTA" class="btn">固件升级</button>
  </view>
</view> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\index\index.wxss ===== 
.container { padding: 40rpx; background-color: #f5f5f5; min-height: 100vh; }
.title { font-size: 40rpx; font-weight: bold; text-align: center; margin: 30rpx 0; }
.status-box { background: white; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; }
.status-item { font-size: 30rpx; margin: 10rpx 0; text-align: center; }
.status-on { color: #07c; font-weight: bold; }
.status-off { color: #666; }
.status-offline { color: #999; }
.btn { margin: 15rpx 0; border-radius: 8rpx; }
.ip-box {
  display: flex;
  align-items: center;
  margin: 15rpx 0;
  background: white;
  border-radius: 16rpx;
  padding: 20rpx;
}
.ip-input {
  flex: 1;
  border: 1rpx solid #ddd;
  padding: 10rpx;
  border-radius: 8rpx;
  font-size: 28rpx;
}
.search-btn {
  margin-left: 20rpx;
}
.ip-box {
  display: flex;
  align-items: center;
  margin: 15rpx 0;
  background: white;
  border-radius: 16rpx;
  padding: 20rpx;
}
.ip-input {
  flex: 1;
  border: 1rpx solid #ddd;
  padding: 10rpx;
  border-radius: 8rpx;
  font-size: 26rpx;
}
.search-btn {
  margin-left: 20rpx;
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\logs\logs.js ===== 
// logs.js
const util = require('../../utils/util.js')

Page({
  data: {
    logs: []
  },
  onLoad() {
    this.setData({
      logs: (wx.getStorageSync('logs') || []).map(log => {
        return {
          date: util.formatTime(new Date(log)),
          timeStamp: log
        }
      })
    })
  }
})
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\logs\logs.json ===== 
{
  "usingComponents": {
  }
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\logs\logs.wxml ===== 
<!--logs.wxml-->
<scroll-view class="scrollarea" scroll-y type="list">
  <block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
    <view class="log-item">{{index + 1}}. {{log.date}}</view>
  </block>
</scroll-view>
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\logs\logs.wxss ===== 
page {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.scrollarea {
  flex: 1;
  overflow-y: hidden;
}
.log-item {
  margin-top: 20rpx;
  text-align: center;
}
.log-item:last-child {
  padding-bottom: env(safe-area-inset-bottom);
}
 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\ota\ota.js ===== 
const app = getApp()
Page({
  data: {
    deviceIP: "",
    isConnected: false,
    filePath: "",
    fileName: "",
    uploading: false,
    progress: 0
  },

  onLoad() {
    // 获取全局设备 IP
    const ip = app.globalData.deviceIP
    this.setData({
      deviceIP: ip || "",
      isConnected: !!ip
    })
  },

  // 选择固件文件
  chooseFile() {
    wx.chooseMessageFile({
      count: 1,
      type: 'file',
      extension: ['.bin'],   // 只显示 .bin 文件
      success: (res) => {
        const file = res.tempFiles[0]
        this.setData({
          filePath: file.path,
          fileName: file.name
        })
      },
      fail: (err) => {
        console.error('选择文件失败', err)
        wx.showToast({ title: '选择文件失败', icon: 'none' })
      }
    })
  },

  // 开始升级
  startOTA() {
    if (!this.data.filePath) {
      wx.showToast({ title: '请先选择固件文件', icon: 'none' })
      return
    }
    if (!this.data.deviceIP) {
      wx.showToast({ title: '未连接设备', icon: 'none' })
      return
    }

    this.setData({ uploading: true, progress: 0 })

    const uploadTask = wx.uploadFile({
      url: `http://${this.data.deviceIP}:8266/update`,
      filePath: this.data.filePath,
      name: 'file',          // ESP OTA 服务要求的字段名
      header: {
        'Content-Type': 'multipart/form-data'
      },
      success: (res) => {
        if (res.statusCode === 200) {
          wx.showModal({
            title: '升级成功',
            content: '设备正在重启,请稍候重新搜索连接。',
            showCancel: false
          })
        } else {
          wx.showToast({ title: '升级失败,请重试', icon: 'none' })
        }
      },
      fail: (err) => {
        console.error('上传失败', err)
        wx.showToast({ title: '上传失败,请检查连接', icon: 'none' })
      },
      complete: () => {
        this.setData({ uploading: false })
      }
    })

    // 监听上传进度
    uploadTask.onProgressUpdate((res) => {
      this.setData({ progress: res.progress })
    })
  }
}) 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\ota\ota.json ===== 
{
  "navigationBarTitleText": "固件升级"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\ota\ota.wxml ===== 
<view class="container">
  <view class="title">固件升级</view>
  <view class="warn">⚠️ 升级过程请勿断电</view>

  <!-- 设备连接状态 -->
  <view class="status-box">
    <text wx:if="{{isConnected}}" class="status-online">● 设备已连接 ({{deviceIP}})</text>
    <text wx:else class="status-offline">● 未连接设备</text>
  </view>

  <!-- 选择文件 -->
  <view class="file-section">
    <button bindtap="chooseFile" class="btn">选择固件文件</button>
    <view class="file-info" wx:if="{{fileName}}">
      <text>已选择:{{fileName}}</text>
    </view>
  </view>

  <!-- 升级按钮 -->
  <button 
    bindtap="startOTA" 
    type="warn" 
    class="btn"
    disabled="{{uploading || !filePath}}"
    loading="{{uploading}}"
  >
    {{uploading ? '上传中' : '开始升级固件'}}
  </button>

  <!-- 进度条 -->
  <view class="progress-box" wx:if="{{uploading}}">
    <progress percent="{{progress}}" show-info stroke-width="12" />
    <text>上传进度:{{progress}}%</text>
  </view>

  <view class="tip">
    * 固件文件为 .bin 格式,可从 Arduino IDE 导出
  </view>
</view> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\ota\ota.wxss ===== 
.container {
  padding: 40rpx;
  text-align: center;
  min-height: 100vh;
  background-color: #f5f5f5;
}
.title {
  font-size: 36rpx;
  margin-bottom: 30rpx;
}
.warn {
  color: red;
  font-size: 28rpx;
  margin-bottom: 40rpx;
}
.status-box {
  margin-bottom: 30rpx;
  font-size: 28rpx;
}
.status-online {
  color: #07c;
}
.status-offline {
  color: #999;
}
.file-section {
  background: white;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 30rpx;
}
.file-info {
  margin-top: 20rpx;
  font-size: 26rpx;
  color: #666;
}
.btn {
  margin-bottom: 30rpx;
  border-radius: 8rpx;
}
.progress-box {
  background: white;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 30rpx;
}
.tip {
  margin-top: 40rpx;
  color: #999;
  font-size: 24rpx;
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\timer\timer.js ===== 
const app = getApp()
Page({
  data: {
    weekTimers: []      // 7 个对象:{ day, label, slots: [{enabled, on, off}, ...] }
  },

  onLoad() {
    this.fetchTimers()
  },

  fetchTimers() {
    if (!app.globalData.deviceIP) {
      wx.showToast({ title: "请先连接设备", icon: "none" })
      return
    }
    wx.request({
      url: `http://${app.globalData.deviceIP}:8266/timers`,
      method: "GET",
      timeout: 3000,
      success: (res) => {
        if (Array.isArray(res.data) && res.data.length === 7) {
          this.setData({ weekTimers: res.data })
        } else {
          this.initDefault()
        }
      },
      fail: () => {
        wx.showToast({ title: "获取定时失败", icon: "none" })
        this.initDefault()
      }
    })
  },

  initDefault() {
    const days = ["周日","周一","周二","周三","周四","周五","周六"]
    const defaultTimers = days.map((label, day) => ({
      day, label,
      slots: []   // 开始无时段
    }))
    this.setData({ weekTimers: defaultTimers })
  },

  // 在指定天添加一个时段
  addSlot(e) {
    const day = e.currentTarget.dataset.day
    const newTimers = this.data.weekTimers.map(item => {
      if (item.day === day) {
        if (item.slots.length >= 4) {
          wx.showToast({ title: "每天最多4个时段", icon: "none" })
          return item   // 不添加
        }
        const slots = [...item.slots, { enabled: true, on: "08:00", off: "22:00" }]
        return { ...item, slots }
      }
      return item
    })
    this.setData({ weekTimers: newTimers })
  },

  // 删除指定天的某个时段
  deleteSlot(e) {
    const { day, index } = e.currentTarget.dataset
    const newTimers = this.data.weekTimers.map(item => {
      if (item.day === day) {
        const slots = item.slots.filter((_, i) => i !== index)
        return { ...item, slots }
      }
      return item
    })
    this.setData({ weekTimers: newTimers })
  },

  // 启用/禁用时段
  toggleSlotEnabled(e) {
    const { day, index } = e.currentTarget.dataset
    const newTimers = this.data.weekTimers.map(item => {
      if (item.day === day) {
        const slots = item.slots.map((s, i) => {
          if (i === index) return { ...s, enabled: !s.enabled }
          return s
        })
        return { ...item, slots }
      }
      return item
    })
    this.setData({ weekTimers: newTimers })
  },

  // 设置开机时间
  setOnTime(e) {
    const { day, index } = e.currentTarget.dataset
    const newTimers = this.data.weekTimers.map(item => {
      if (item.day === day) {
        const slots = item.slots.map((s, i) => {
          if (i === index) return { ...s, on: e.detail.value }
          return s
        })
        return { ...item, slots }
      }
      return item
    })
    this.setData({ weekTimers: newTimers })
  },

  // 设置关机时间
  setOffTime(e) {
    const { day, index } = e.currentTarget.dataset
    const newTimers = this.data.weekTimers.map(item => {
      if (item.day === day) {
        const slots = item.slots.map((s, i) => {
          if (i === index) return { ...s, off: e.detail.value }
          return s
        })
        return { ...item, slots }
      }
      return item
    })
    this.setData({ weekTimers: newTimers })
  },

  saveTimers() {
    if (!app.globalData.deviceIP) {
      wx.showToast({ title: "未连接设备", icon: "none" })
      return
    }
    // 整理发送数据,只保留必要字段
    const payload = this.data.weekTimers.map(({ day, slots }) => ({
      day,
      slots: slots.map(({ enabled, on, off }) => ({ enabled, on, off }))
    }))

    wx.request({
      url: `http://${app.globalData.deviceIP}:8266/timers`,
      method: "POST",
      data: payload,
      header: { 'content-type': 'application/json' },
      timeout: 3000,
      success: (res) => {
        if (res.data && res.data.result === "ok") {
          wx.showToast({ title: "保存成功", icon: "success" })
          setTimeout(() => wx.navigateBack(), 1500)
        } else {
          wx.showToast({ title: "保存失败", icon: "none" })
        }
      },
      fail: () => {
        wx.showToast({ title: "请求失败", icon: "none" })
      }
    })
  }
}) 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\timer\timer.json ===== 
{
  "navigationBarTitleText": "定时设置"
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\timer\timer.wxml ===== 
<view class="container">
  <view class="title">一周定时设置</view>

  <view wx:for="{{weekTimers}}" wx:key="day" class="day-card">
    <view class="day-header">
      <text class="day-label">{{item.label}}</text>
      <button size="mini" bindtap="addSlot" data-day="{{item.day}}" class="add-btn"wx:if="{{item.slots.length < 4}}">+ 添加时段</button>
    </view>

    <!-- 时段列表 -->
    <view wx:for="{{item.slots}}" wx:key="index" wx:for-item="slot" wx:for-index="idx" class="slot-box">
      <view class="slot-header">
        <text>时段 {{idx + 1}}</text>
        <view class="slot-actions">
          <switch checked="{{slot.enabled}}" bindchange="toggleSlotEnabled" data-day="{{item.day}}" data-index="{{idx}}" />
          <button size="mini" bindtap="deleteSlot" data-day="{{item.day}}" data-index="{{idx}}" class="delete-btn">删除</button>
        </view>
      </view>
      <view class="time-row">
        <text>开机:</text>
        <picker mode="time" value="{{slot.on}}" bindchange="setOnTime" data-day="{{item.day}}" data-index="{{idx}}">
          <view class="picker-text">{{slot.on}}</view>
        </picker>
      </view>
      <view class="time-row">
        <text>关机:</text>
        <picker mode="time" value="{{slot.off}}" bindchange="setOffTime" data-day="{{item.day}}" data-index="{{idx}}">
          <view class="picker-text">{{slot.off}}</view>
        </picker>
      </view>
    </view>
    <view wx:if="{{item.slots.length === 0}}" class="empty-tip">暂无时段</view>
  </view>

  <button bindtap="saveTimers" type="primary" class="save-btn">保存一周定时</button>
</view> 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\pages\timer\timer.wxss ===== 
.container {
  padding: 30rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
}
.title {
  font-size: 36rpx;
  font-weight: bold;
  text-align: center;
  margin-bottom: 30rpx;
}
.day-card {
  background: white;
  border-radius: 16rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
}
.day-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15rpx;
}
.day-label {
  font-size: 30rpx;
  font-weight: bold;
}
.add-btn {
  font-size: 24rpx;
}
.slot-box {
  background: #f9f9f9;
  border-radius: 12rpx;
  padding: 15rpx;
  margin-bottom: 15rpx;
}
.slot-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10rpx;
  font-size: 28rpx;
  font-weight: bold;
}
.slot-actions {
  display: flex;
  align-items: center;
}
.delete-btn {
  margin-left: 15rpx;
  color: red;
}
.time-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8rpx 0;
  font-size: 28rpx;
}
.picker-text {
  color: #07c;
}
.empty-tip {
  text-align: center;
  color: #999;
  font-size: 26rpx;
  padding: 10rpx 0;
}
.save-btn {
  margin-top: 40rpx;
  border-radius: 8rpx;
} 
------------------------------------------------ 
 
===== \Users\Administrator\WeChatProjects\miniprogram-1\utils\util.js ===== 
const formatTime = date => {
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  const second = date.getSeconds()
  return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
}
const formatNumber = n => {
  n = n.toString()
  return n[1] ? n : `0${n}`
}
module.exports = { formatTime } 
------------------------------------------------ 
 

3.ESP32串口输出

bash 复制代码
[20:41:27.089]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:28.090]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:29.090]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:30.090]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:31.090]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:32.090]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:33.089]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:34.089]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:35.089]收←◆[STA] Relay=OFF IP:192.168.10.153

[20:41:36.089]收←◆[STA] Relay=OFF IP:192.168.10.153

4.1.连接ESP发出的热点

4.2.点击搜索设备、输入WIFI账号密码、等待重启后连接同一WiFi即刚才输入的WIFI信息、

相关推荐
A24207349305 小时前
微信小程序开发:常用基础语法与指令详解
微信小程序·小程序·notepad++
黄华SJ520it6 小时前
青蓝送水类型社交裂变小程序系统技术拆解
小程序·系统开发
weikecms17 小时前
星巴克API接口,星巴克点餐接口开发
大数据·小程序
游戏开发爱好者821 小时前
系统状态:专为iPhone和iPad设计的实时系统性能监控工具全面介绍
android·ios·小程序·uni-app·iphone·webview·ipad
2501_915909061 天前
全面解析iOS应用上架到App Store的完整流程与关键注意事项
android·macos·ios·小程序·uni-app·cocoa·iphone
千舟软件1 天前
千舟软件介绍 | 顺便聊聊我们做了哪些产品
大数据·数据库·科技·小程序·业界资讯
laboratory agent开发1 天前
2026小程序系统多维度技术与落地能力深度解析
微信小程序
2501_915106321 天前
iOS 证书类型及作用说明 账号证书与签名配置的完整解读
android·ios·小程序·https·uni-app·iphone·webview
凡泰AI1 天前
如何通过小程序多端框架,让一个小程序同时运行在iOS、安卓、鸿蒙以及PC客户端,实现一次开发多端运行的效果
android·ios·小程序