|
|
@@ -0,0 +1,824 @@
|
|
|
+#include "WiFiConfigurator.h"
|
|
|
+
|
|
|
+// 静态实例指针初始化
|
|
|
+WiFiConfigurator* WiFiConfigurator::_instance = nullptr;
|
|
|
+
|
|
|
+// HTML页面内容
|
|
|
+const char captive_html[] PROGMEM = R"rawliteral(
|
|
|
+<!DOCTYPE HTML>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta http-equiv="refresh" content="0;url=/">
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+)rawliteral";
|
|
|
+
|
|
|
+const char index_html[] PROGMEM = R"rawliteral(
|
|
|
+<!DOCTYPE HTML>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>WiFi配置</title>
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <style>
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
|
+ body { background-color: #f5f5f7; color: #333; padding: 20px; }
|
|
|
+ .container { max-width: 500px; margin: 50px auto; background: white; padding: 30px; border-radius: 18px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); }
|
|
|
+ h1 { color: #1d1d1f; text-align: center; margin-bottom: 25px; font-weight: 600; }
|
|
|
+ .form-group { margin-bottom: 20px; }
|
|
|
+ .wifi-selector { display: flex; gap: 10px; align-items: flex-end; }
|
|
|
+ label { display: block; margin-bottom: 8px; font-weight: 500; color: #6e6e73; }
|
|
|
+ select, input { width: 100%; padding: 14px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 16px; }
|
|
|
+ select:focus, input:focus { outline: none; border-color: #0071e3; box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.2); }
|
|
|
+ button { background-color: #0071e3; color: white; border: none; cursor: pointer; font-weight: 500; padding: 14px; border-radius: 10px; margin-top: 10px; }
|
|
|
+ button:hover { background-color: #0077ed; }
|
|
|
+ button:disabled { background-color: #94bfff; cursor: not-allowed; }
|
|
|
+ .refresh-btn { padding: 14px; width: auto; white-space: nowrap; }
|
|
|
+ .connect-btn { width: 100%; }
|
|
|
+ .signal { display: inline-block; width: 80px; text-align: right; color: #86868b; }
|
|
|
+ .status-bar { text-align: center; margin-top: 20px; color: #6e6e73; font-size: 14px; }
|
|
|
+ .message { padding: 15px; margin: 15px 0; border-radius: 8px; text-align: center; }
|
|
|
+ .success { background-color: #eaf8ed; color: #147d39; }
|
|
|
+ .error { background-color: #fee; color: #c51e24; }
|
|
|
+ .loading { background-color: #f0f7ff; color: #0066cc; }
|
|
|
+ .password-hint { font-size: 12px; color: #86868b; margin-top: 5px; }
|
|
|
+ .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(0,113,227,.3); border-radius: 50%; border-top-color: #0071e3; animation: spin 1s ease-in-out infinite; margin-right: 8px; }
|
|
|
+ @keyframes spin { to { transform: rotate(360deg); } }
|
|
|
+ .connection-status { margin-top: 15px; padding: 10px; border-radius: 8px; background-color: #f5f5f7; text-align: center; font-size: 14px; }
|
|
|
+ .progress-container { margin-top: 10px; background-color: #eaeaea; border-radius: 10px; overflow: hidden; }
|
|
|
+ .progress-bar { height: 8px; background-color: #0071e3; width: 0%; transition: width 0.3s ease; }
|
|
|
+ .timeout-warning { color: #ff9500; font-size: 12px; text-align: center; margin-top: 10px; }
|
|
|
+ </style>
|
|
|
+ <script>
|
|
|
+ let checkStatusInterval;
|
|
|
+ let timeoutTimer;
|
|
|
+ let remainingTime = 120; // 120秒超时
|
|
|
+
|
|
|
+ // 更新超时提示
|
|
|
+ function updateTimeoutDisplay() {
|
|
|
+ const element = document.getElementById('timeoutWarning');
|
|
|
+ if (element) {
|
|
|
+ element.textContent = `配置热点将在 ${remainingTime} 秒后自动关闭`;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (remainingTime <= 0) {
|
|
|
+ clearInterval(timeoutTimer);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ remainingTime--;
|
|
|
+ }
|
|
|
+
|
|
|
+ function startCheckingStatus() {
|
|
|
+ // 禁用表单防止重复提交
|
|
|
+ document.getElementById('connectBtn').disabled = true;
|
|
|
+ document.getElementById('ssid').disabled = true;
|
|
|
+ document.getElementById('password').disabled = true;
|
|
|
+ document.querySelector('.refresh-btn').disabled = true;
|
|
|
+
|
|
|
+ // 重置超时时间
|
|
|
+ resetTimeout();
|
|
|
+
|
|
|
+ // 显示初始状态
|
|
|
+ updateStatus({
|
|
|
+ connecting: true,
|
|
|
+ status: "准备连接...",
|
|
|
+ progress: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ // 每200ms检查一次状态
|
|
|
+ checkStatusInterval = setInterval(() => {
|
|
|
+ fetch('/status')
|
|
|
+ .then(response => {
|
|
|
+ if (!response.ok) throw new Error('网络错误');
|
|
|
+ return response.json();
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ updateStatus(data);
|
|
|
+
|
|
|
+ // 如果连接完成,停止检查
|
|
|
+ if (!data.connecting) {
|
|
|
+ clearInterval(checkStatusInterval);
|
|
|
+
|
|
|
+ // 连接成功时显示提示并跳转
|
|
|
+ if (data.success) {
|
|
|
+ setTimeout(() => {
|
|
|
+ window.location.href = '/success';
|
|
|
+ }, 1000);
|
|
|
+ } else {
|
|
|
+ // 连接失败时重新启用表单,允许重试
|
|
|
+ enableForm();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ console.error('获取状态失败:', err);
|
|
|
+ updateStatus({
|
|
|
+ connecting: false,
|
|
|
+ status: "通信错误,请重试",
|
|
|
+ progress: 0
|
|
|
+ });
|
|
|
+ clearInterval(checkStatusInterval);
|
|
|
+ enableForm();
|
|
|
+ });
|
|
|
+ }, 200);
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateStatus(data) {
|
|
|
+ const statusElement = document.getElementById('connectionStatus');
|
|
|
+ const progressBar = document.getElementById('progressBar');
|
|
|
+
|
|
|
+ statusElement.innerHTML = data.connecting ?
|
|
|
+ '<span class="spinner"></span>' + data.status : data.status;
|
|
|
+
|
|
|
+ statusElement.className = 'connection-status ' +
|
|
|
+ (data.connecting ? 'loading' : (data.success ? 'success' : 'error'));
|
|
|
+
|
|
|
+ progressBar.style.width = data.progress + '%';
|
|
|
+ }
|
|
|
+
|
|
|
+ function enableForm() {
|
|
|
+ // 确保表单元素可操作
|
|
|
+ document.getElementById('connectBtn').disabled = false;
|
|
|
+ document.getElementById('ssid').disabled = false;
|
|
|
+ document.getElementById('password').disabled = false;
|
|
|
+ document.querySelector('.refresh-btn').disabled = false;
|
|
|
+
|
|
|
+ // 聚焦到密码输入框
|
|
|
+ document.getElementById('password').focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置超时计时器
|
|
|
+ function resetTimeout() {
|
|
|
+ remainingTime = 120;
|
|
|
+ clearInterval(timeoutTimer);
|
|
|
+ timeoutTimer = setInterval(updateTimeoutDisplay, 1000);
|
|
|
+ updateTimeoutDisplay();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 密码验证
|
|
|
+ function validatePassword() {
|
|
|
+ const password = document.getElementById('password').value;
|
|
|
+ const hintElement = document.getElementById('passwordHint');
|
|
|
+
|
|
|
+ if (password.length === 0) {
|
|
|
+ hintElement.textContent = '请输入WiFi密码';
|
|
|
+ hintElement.style.color = '#86868b';
|
|
|
+ return true;
|
|
|
+ } else if (password.length < 8 && password.length > 0) {
|
|
|
+ hintElement.textContent = '警告: 大多数WiFi密码至少需要8个字符';
|
|
|
+ hintElement.style.color = '#ff9500';
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ hintElement.textContent = '';
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ window.onload = function() {
|
|
|
+ // 初始化超时显示
|
|
|
+ const timeoutDiv = document.createElement('div');
|
|
|
+ timeoutDiv.id = 'timeoutWarning';
|
|
|
+ timeoutDiv.className = 'timeout-warning';
|
|
|
+ document.querySelector('.container').appendChild(timeoutDiv);
|
|
|
+ resetTimeout();
|
|
|
+
|
|
|
+ const passwordInput = document.getElementById('password');
|
|
|
+ passwordInput.addEventListener('input', validatePassword);
|
|
|
+
|
|
|
+ const connectForm = document.getElementById('connectForm');
|
|
|
+ connectForm.addEventListener('submit', function(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ if (validatePassword()) {
|
|
|
+ // 提交表单数据到后端
|
|
|
+ const formData = new FormData(connectForm);
|
|
|
+ fetch('/connect', {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ }).then(() => {
|
|
|
+ startCheckingStatus();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 为刷新按钮添加超时重置
|
|
|
+ document.querySelector('.refresh-btn').addEventListener('click', function() {
|
|
|
+ resetTimeout();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1>WiFi网络配置</h1>
|
|
|
+
|
|
|
+ %MESSAGE_BOX%
|
|
|
+
|
|
|
+ <form id="connectForm">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="ssid">选择WiFi网络:</label>
|
|
|
+ <div class="wifi-selector">
|
|
|
+ <select id="ssid" name="ssid" required style="flex: 1;">
|
|
|
+ %WIFI_LIST%
|
|
|
+ </select>
|
|
|
+ <button type="button" class="refresh-btn" onclick="window.location.href='/refresh'">
|
|
|
+ 刷新
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="password">WiFi密码:</label>
|
|
|
+ <input type="password" id="password" name="password" placeholder="请输入WiFi密码">
|
|
|
+ <div id="passwordHint" class="password-hint"></div>
|
|
|
+ </div>
|
|
|
+ <button type="submit" id="connectBtn" class="connect-btn">
|
|
|
+ 连接网络
|
|
|
+ </button>
|
|
|
+ <div id="connectionStatus" class="connection-status">
|
|
|
+ 等待操作...
|
|
|
+ </div>
|
|
|
+ <div class="progress-container">
|
|
|
+ <div id="progressBar" class="progress-bar"></div>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ <div class="status-bar">
|
|
|
+ 设备热点: %AP_SSID%
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+)rawliteral";
|
|
|
+
|
|
|
+const char success_html[] PROGMEM = R"rawliteral(
|
|
|
+<!DOCTYPE HTML>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>连接成功</title>
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <style>
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
|
+ body { background-color: #f5f5f7; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; color: #333; }
|
|
|
+ .container { text-align: center; max-width: 400px; width: 100%; padding: 30px; background: white; border-radius: 18px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); }
|
|
|
+ h1 { margin-bottom: 20px; font-weight: 600; color: #147d39; }
|
|
|
+ .message { margin: 20px 0; line-height: 1.6; color: #6e6e73; }
|
|
|
+ .countdown { color: #0071e3; font-weight: 600; margin: 15px 0; }
|
|
|
+ </style>
|
|
|
+ <script>
|
|
|
+ // 5秒后提示用户切换网络
|
|
|
+ let countdown = 5;
|
|
|
+ function updateCountdown() {
|
|
|
+ document.getElementById('countdown').textContent = countdown;
|
|
|
+ if (countdown <= 0) {
|
|
|
+ document.getElementById('countdownMessage').textContent =
|
|
|
+ '请切换到已配置的WiFi网络';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ countdown--;
|
|
|
+ setTimeout(updateCountdown, 1000);
|
|
|
+ }
|
|
|
+ window.onload = updateCountdown;
|
|
|
+ </script>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1>连接成功!</h1>
|
|
|
+
|
|
|
+ <div class="message">
|
|
|
+ <p>已成功连接到 %SSID%</p>
|
|
|
+ <p>IP地址: %IP_ADDRESS%</p>
|
|
|
+ <div id="countdownMessage" class="countdown">
|
|
|
+ 配置热点将在 <span id="countdown">5</span> 秒后关闭
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+)rawliteral";
|
|
|
+
|
|
|
+const char error_html[] PROGMEM = R"rawliteral(
|
|
|
+<!DOCTYPE HTML>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>连接失败</title>
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <style>
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
|
+ body { background-color: #f5f5f7; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; color: #333; }
|
|
|
+ .container { text-align: center; max-width: 400px; width: 100%; padding: 30px; background: white; border-radius: 18px; box-shadow: 0 4px 20px rgba(0,0,0,0.05); }
|
|
|
+ h1 { margin-bottom: 20px; font-weight: 600; color: #c51e24; }
|
|
|
+ .message { margin: 20px 0; line-height: 1.6; color: #6e6e73; }
|
|
|
+ button { background-color: #0071e3; color: white; border: none; cursor: pointer; font-weight: 500; padding: 14px 24px; border-radius: 10px; margin-top: 20px; font-size: 16px; }
|
|
|
+ button:hover { background-color: #0077ed; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1>连接失败</h1>
|
|
|
+
|
|
|
+ <div class="message">
|
|
|
+ <p>无法连接到 %SSID%</p>
|
|
|
+ <p>%ERROR_MESSAGE%</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button onclick="window.location.href='/'">返回重试</button>
|
|
|
+ </div>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+)rawliteral";
|
|
|
+
|
|
|
+// 构造函数
|
|
|
+WiFiConfigurator::WiFiConfigurator(int slowLedPin, int fastLedPin, int configButtonPin,
|
|
|
+ const char* ap_ssid, const char* ap_password,
|
|
|
+ IPAddress apIP, IPAddress gateway, IPAddress subnet)
|
|
|
+ : _slowLedPin(slowLedPin),
|
|
|
+ _fastLedPin(fastLedPin),
|
|
|
+ _configButtonPin(configButtonPin),
|
|
|
+ _ap_ssid(ap_ssid),
|
|
|
+ _ap_password(ap_password),
|
|
|
+ _apIP(apIP),
|
|
|
+ _gateway(gateway),
|
|
|
+ _subnet(subnet),
|
|
|
+ _server(80),
|
|
|
+ _captive_html(captive_html),
|
|
|
+ _index_html(index_html),
|
|
|
+ _success_html(success_html),
|
|
|
+ _error_html(error_html) {
|
|
|
+ _instance = this;
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化函数
|
|
|
+void WiFiConfigurator::begin() {
|
|
|
+ // 初始化LED
|
|
|
+ pinMode(_slowLedPin, OUTPUT);
|
|
|
+ pinMode(_fastLedPin, OUTPUT);
|
|
|
+
|
|
|
+ // 初始化按键,使用内部上拉电阻
|
|
|
+ pinMode(_configButtonPin, INPUT_PULLUP);
|
|
|
+
|
|
|
+ Serial.println("WiFi配置器初始化...");
|
|
|
+
|
|
|
+ _preferences.begin("wifi-config", false);
|
|
|
+
|
|
|
+ // 启动时立即扫描WiFi
|
|
|
+ scanWiFiNetworks();
|
|
|
+
|
|
|
+ // 尝试连接已保存的WiFi
|
|
|
+ String savedSsid = _preferences.getString("ssid", "");
|
|
|
+ String savedPassword = _preferences.getString("password", "");
|
|
|
+
|
|
|
+ if (savedSsid.length() > 0) {
|
|
|
+ Serial.println("尝试连接已保存的WiFi...");
|
|
|
+ connectToWiFi(savedSsid.c_str(), savedPassword.c_str());
|
|
|
+ } else {
|
|
|
+ startAPMode();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 主循环处理函数
|
|
|
+void WiFiConfigurator::loop() {
|
|
|
+ // 处理按键输入
|
|
|
+ handleConfigButton();
|
|
|
+
|
|
|
+ // LED控制
|
|
|
+ unsigned long currentMillis = millis();
|
|
|
+
|
|
|
+ // 慢闪LED
|
|
|
+ if (currentMillis - _previousSlowMillis >= _slowInterval) {
|
|
|
+ _previousSlowMillis = currentMillis;
|
|
|
+ _slowLedState = !_slowLedState;
|
|
|
+ digitalWrite(_slowLedPin, _slowLedState);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 快闪LED - 连接过程中加速闪烁
|
|
|
+ if (_connectionInProgress) {
|
|
|
+ if (currentMillis - _previousFastMillis >= 100) { // 加速闪烁
|
|
|
+ _previousFastMillis = currentMillis;
|
|
|
+ _fastLedState = !_fastLedState;
|
|
|
+ digitalWrite(_fastLedPin, _fastLedState);
|
|
|
+ }
|
|
|
+ } else if (WiFi.getMode() == WIFI_AP || WiFi.status() != WL_CONNECTED) {
|
|
|
+ if (currentMillis - _previousFastMillis >= _fastInterval) {
|
|
|
+ _previousFastMillis = currentMillis;
|
|
|
+ _fastLedState = !_fastLedState;
|
|
|
+ digitalWrite(_fastLedPin, _fastLedState);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ digitalWrite(_fastLedPin, HIGH);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 持续处理网络请求
|
|
|
+ if (WiFi.getMode() & WIFI_AP) { // 只要AP模式开启就处理请求
|
|
|
+ _dnsServer.processNextRequest();
|
|
|
+ _server.handleClient();
|
|
|
+
|
|
|
+ // 检查AP模式超时(仅在连接未进行时)
|
|
|
+ if (!_connectionInProgress && _apTimeout > 0 && millis() - _apTimeout > _apTimeoutDuration) {
|
|
|
+ Serial.println("AP模式超时,关闭热点");
|
|
|
+ WiFi.softAPdisconnect(true);
|
|
|
+ _server.stop();
|
|
|
+ _dnsServer.stop();
|
|
|
+ _apTimeout = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 连接成功后保持AP模式一段时间
|
|
|
+ if (_connectionSuccess && (WiFi.getMode() & WIFI_AP) &&
|
|
|
+ millis() - _successNotificationTime > _successNotificationDelay) {
|
|
|
+ Serial.println("成功通知时间已到,关闭AP模式");
|
|
|
+ WiFi.softAPdisconnect(true);
|
|
|
+ _server.stop();
|
|
|
+ _dnsServer.stop();
|
|
|
+ _apTimeout = 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 检查是否已连接WiFi
|
|
|
+bool WiFiConfigurator::isConnected() {
|
|
|
+ return WiFi.status() == WL_CONNECTED;
|
|
|
+}
|
|
|
+
|
|
|
+// 获取当前连接的SSID
|
|
|
+String WiFiConfigurator::getCurrentSSID() {
|
|
|
+ return WiFi.status() == WL_CONNECTED ? WiFi.SSID() : "";
|
|
|
+}
|
|
|
+
|
|
|
+// 获取IP地址
|
|
|
+String WiFiConfigurator::getIPAddress() {
|
|
|
+ return WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "";
|
|
|
+}
|
|
|
+
|
|
|
+// 处理配置按键
|
|
|
+
|
|
|
+void WiFiConfigurator::handleConfigButton() {
|
|
|
+ // 读取按键状态(LOW表示按下,因为使用了内部上拉)
|
|
|
+ int buttonState = digitalRead(_configButtonPin);
|
|
|
+ // static bool slowLedAlwaysOn = false; // 标记慢闪灯是否已常亮
|
|
|
+
|
|
|
+ // 按键按下
|
|
|
+ if (buttonState == LOW && !_buttonPressed) {
|
|
|
+ _buttonPressed = true;
|
|
|
+ _buttonPressStartTime = millis();
|
|
|
+ // slowLedAlwaysOn = false; // 重置状态
|
|
|
+ Serial.println("配置按键被按下...");
|
|
|
+ // 开始快速闪烁LED,表示正在计时
|
|
|
+ _fastLedState = HIGH;
|
|
|
+ digitalWrite(_fastLedPin, _fastLedState);
|
|
|
+ }
|
|
|
+ // 按键释放
|
|
|
+ else if (buttonState == HIGH && _buttonPressed) {
|
|
|
+ unsigned long pressDuration = millis() - _buttonPressStartTime;
|
|
|
+ _buttonPressed = false;
|
|
|
+ // slowLedAlwaysOn = false; // 重置状态
|
|
|
+
|
|
|
+ // 检查按下时间是否达到要求
|
|
|
+ if (pressDuration >= _configButtonPressDuration) {
|
|
|
+ Serial.println("按键长按5秒以上,进入配网模式...");
|
|
|
+ enterConfigMode();
|
|
|
+ } else {
|
|
|
+ Serial.printf("按键释放,按下时间: %d毫秒(不足5秒)\n", pressDuration);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 按键持续按下中
|
|
|
+ else if (buttonState == LOW && _buttonPressed) {
|
|
|
+ unsigned long pressDuration = millis() - _buttonPressStartTime;
|
|
|
+
|
|
|
+ // 每100ms检查一次(提高响应速度)
|
|
|
+ if (pressDuration % 100 < 10) {
|
|
|
+ // 计算已按下的秒数
|
|
|
+ int secondsPressed = pressDuration / 1000;
|
|
|
+ Serial.printf("按键持续按下中: %d秒/%d秒\n", secondsPressed, _configButtonPressDuration / 1000);
|
|
|
+
|
|
|
+ // 快闪LED提示用户还在计时
|
|
|
+ _fastLedState = !_fastLedState;
|
|
|
+ digitalWrite(_fastLedPin, _fastLedState);
|
|
|
+
|
|
|
+ // 关键修改:按住期间达到5秒且尚未设置常亮
|
|
|
+ if (pressDuration >= _configButtonPressDuration) {
|
|
|
+ // if (pressDuration >= _configButtonPressDuration && !slowLedAlwaysOn) {
|
|
|
+ Serial.println("已按住5秒,慢闪灯常亮");
|
|
|
+ _slowLedState = HIGH; // 设置慢闪LED状态为常亮
|
|
|
+ digitalWrite(_slowLedPin, _slowLedState);
|
|
|
+ // slowLedAlwaysOn = true; // 标记已常亮,避免重复设置
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 进入配网模式
|
|
|
+void WiFiConfigurator::enterConfigMode() {
|
|
|
+ // 清除已保存的WiFi配置
|
|
|
+ _preferences.putString("ssid", "");
|
|
|
+ _preferences.putString("password", "");
|
|
|
+
|
|
|
+ // 停止当前的WiFi连接
|
|
|
+ WiFi.disconnect();
|
|
|
+
|
|
|
+ // 重新启动AP模式和Web服务
|
|
|
+ startAPMode();
|
|
|
+
|
|
|
+ // 闪烁LED提示进入配网模式
|
|
|
+ for (int i = 0; i < 5; i++) {
|
|
|
+ digitalWrite(_slowLedPin, HIGH);
|
|
|
+ digitalWrite(_fastLedPin, HIGH);
|
|
|
+ delay(200);
|
|
|
+ digitalWrite(_slowLedPin, LOW);
|
|
|
+ digitalWrite(_fastLedPin, LOW);
|
|
|
+ delay(200);
|
|
|
+ }
|
|
|
+
|
|
|
+ Serial.println("已进入配网模式,请连接设备热点进行配置");
|
|
|
+}
|
|
|
+
|
|
|
+// 扫描WiFi网络并缓存结果
|
|
|
+void WiFiConfigurator::scanWiFiNetworks() {
|
|
|
+ Serial.println("开始扫描WiFi网络...");
|
|
|
+ int n = WiFi.scanNetworks(false, false, false, 100);
|
|
|
+ Serial.printf("发现 %d 个WiFi网络\n", n);
|
|
|
+
|
|
|
+ _wifiScanResults = "";
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ int signalStrength = map(WiFi.RSSI(i), -100, -50, 0, 4);
|
|
|
+ String signalBar = "";
|
|
|
+ for(int j = 0; j < signalStrength; j++) signalBar += "●";
|
|
|
+ for(int j = signalStrength; j < 4; j++) signalBar += "○";
|
|
|
+
|
|
|
+ _wifiScanResults += "<option value=\"" + WiFi.SSID(i) + "\">" +
|
|
|
+ WiFi.SSID(i) + " <span class=\"signal\">" + signalBar + "</span></option>";
|
|
|
+
|
|
|
+ // 串口打印扫描结果
|
|
|
+ Serial.printf("网络: %s, 信号强度: %d, 加密方式: %d\n",
|
|
|
+ WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.encryptionType(i));
|
|
|
+ }
|
|
|
+ WiFi.scanDelete();
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::startAPMode() {
|
|
|
+ Serial.println("启动AP模式...");
|
|
|
+
|
|
|
+ // 重置连接状态
|
|
|
+ _connectionInProgress = false;
|
|
|
+ _connectionCompleted = false;
|
|
|
+ _connectionSuccess = false;
|
|
|
+ _connectionStatus = "";
|
|
|
+ _connectionProgress = 0;
|
|
|
+
|
|
|
+ // 配置为AP模式
|
|
|
+ WiFi.mode(WIFI_AP);
|
|
|
+ delay(200); // 增加延迟确保模式切换完成
|
|
|
+ bool configSuccess = WiFi.softAPConfig(_apIP, _gateway, _subnet);
|
|
|
+ Serial.print("AP配置: ");
|
|
|
+ Serial.println(configSuccess ? "成功" : "失败");
|
|
|
+
|
|
|
+ // 启动AP
|
|
|
+ bool apStarted = WiFi.softAP(_ap_ssid, _ap_password, 1, false, 4);
|
|
|
+ if (!apStarted) {
|
|
|
+ Serial.println("AP启动失败,尝试默认配置...");
|
|
|
+ apStarted = WiFi.softAP(_ap_ssid, _ap_password);
|
|
|
+ }
|
|
|
+ Serial.print("AP启动: ");
|
|
|
+ Serial.println(apStarted ? "成功" : "失败");
|
|
|
+
|
|
|
+ // 配置DNS
|
|
|
+ _dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
|
|
|
+ bool dnsStarted = _dnsServer.start(_dnsPort, "*", _apIP);
|
|
|
+ Serial.print("DNS服务器启动: ");
|
|
|
+ Serial.println(dnsStarted ? "成功" : "失败");
|
|
|
+
|
|
|
+ // 苹果Captive Portal支持
|
|
|
+ _server.on("/hotspot-detect.html", [](){
|
|
|
+ _instance->_server.send(200, "text/html", _instance->_captive_html);
|
|
|
+ });
|
|
|
+ _server.on("/library/test/success.html", [](){
|
|
|
+ _instance->_server.send(200, "text/html", _instance->_captive_html);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 主要路由
|
|
|
+ _server.on("/", handleRoot);
|
|
|
+ _server.on("/refresh", handleRefresh);
|
|
|
+ _server.on("/connect", HTTP_POST, handleConnect);
|
|
|
+ _server.on("/status", handleStatus);
|
|
|
+ _server.on("/success", handleSuccess);
|
|
|
+ _server.on("/error", handleError);
|
|
|
+ _server.onNotFound(handleNotFound);
|
|
|
+
|
|
|
+ _server.begin();
|
|
|
+ Serial.print("AP IP地址: ");
|
|
|
+ Serial.println(WiFi.softAPIP());
|
|
|
+
|
|
|
+ // 重置AP超时计时器
|
|
|
+ _apTimeout = millis();
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::connectToWiFi(const char* ssid, const char* password) {
|
|
|
+ _currentSsid = ssid;
|
|
|
+
|
|
|
+ _connectionInProgress = true;
|
|
|
+ _connectionCompleted = false;
|
|
|
+ _connectionSuccess = false;
|
|
|
+ _connectionProgress = 0;
|
|
|
+
|
|
|
+ Serial.println("\n===== [DEBUG] 开始连接WiFi流程 =====\n");
|
|
|
+
|
|
|
+ // 完全断开所有WiFi连接
|
|
|
+ Serial.println("[DEBUG] 1. 正在清除之前的WiFi连接信息...");
|
|
|
+ WiFi.disconnect(true);
|
|
|
+ delay(500); // 增加一点延迟,确保清除操作完成
|
|
|
+
|
|
|
+ // 配置为AP+STA模式
|
|
|
+ Serial.println("[DEBUG] 2. 设置WiFi模式为 AP+STA...");
|
|
|
+ WiFi.mode(WIFI_AP_STA);
|
|
|
+ delay(200);
|
|
|
+
|
|
|
+ // 打印将要使用的SSID和密码
|
|
|
+ Serial.print("[DEBUG] 3. 准备连接到网络...");
|
|
|
+ Serial.print(" SSID: '");
|
|
|
+ Serial.print(ssid);
|
|
|
+ Serial.print("' ");
|
|
|
+ Serial.print(" Password: '");
|
|
|
+ Serial.print(password);
|
|
|
+ Serial.println("'");
|
|
|
+
|
|
|
+ // 开始连接
|
|
|
+ Serial.println("[DEBUG] 4. 调用 WiFi.begin()...");
|
|
|
+ WiFi.begin(ssid, password);
|
|
|
+
|
|
|
+ // 等待连接结果(最多20秒)
|
|
|
+ Serial.println("[DEBUG] 5. 开始等待连接结果...");
|
|
|
+ unsigned long startAttemptTime = millis();
|
|
|
+ while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 20000) {
|
|
|
+
|
|
|
+ // 打印当前状态码
|
|
|
+ Serial.printf("[DEBUG] 状态码: %d (等待连接中...)\n", WiFi.status());
|
|
|
+
|
|
|
+ int elapsedSeconds = (millis() - startAttemptTime) / 1000;
|
|
|
+ _connectionProgress = 20 + (elapsedSeconds * 70) / 20;
|
|
|
+ if (_connectionProgress > 90) _connectionProgress = 90;
|
|
|
+
|
|
|
+ _dnsServer.processNextRequest();
|
|
|
+ _server.handleClient();
|
|
|
+
|
|
|
+ delay(1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查最终连接结果
|
|
|
+ Serial.println("\n[DEBUG] 6. 连接尝试结束,检查最终结果...");
|
|
|
+ if (WiFi.status() == WL_CONNECTED) {
|
|
|
+ _connectionSuccess = true;
|
|
|
+ String ipAddress = WiFi.localIP().toString();
|
|
|
+ _connectionStatus = "成功连接到 " + _currentSsid + ",IP地址: " + ipAddress;
|
|
|
+ Serial.println("[DEBUG] => 连接成功!");
|
|
|
+ Serial.printf("===== [DEBUG] 成功连接到 '%s',IP: %s =====\n", ssid, ipAddress.c_str());
|
|
|
+
|
|
|
+ _preferences.putString("ssid", ssid);
|
|
|
+ _preferences.putString("password", password);
|
|
|
+
|
|
|
+ _connectionCompleted = true;
|
|
|
+ _connectionInProgress = false;
|
|
|
+ _connectionProgress = 100; // 标记进度为100%
|
|
|
+
|
|
|
+ // 不立即关闭AP,而是记录成功时间,稍后在loop中关闭
|
|
|
+ _successNotificationTime = millis();
|
|
|
+ Serial.println("连接成功,保持AP模式5秒以确保客户端收到通知");
|
|
|
+
|
|
|
+ } else {
|
|
|
+ _connectionSuccess = false;
|
|
|
+ int errorCode = WiFi.status();
|
|
|
+ Serial.println("[DEBUG] => 连接失败!");
|
|
|
+ Serial.printf("[DEBUG] 最终状态码: %d\n", errorCode);
|
|
|
+
|
|
|
+ String errorMsg;
|
|
|
+ switch(errorCode) {
|
|
|
+ case WL_CONNECT_FAILED: errorMsg = "连接失败(密码错误或网络拒绝接入)"; break;
|
|
|
+ case WL_CONNECTION_LOST: errorMsg = "连接丢失"; break;
|
|
|
+ case WL_DISCONNECTED: errorMsg = "已断开连接"; break;
|
|
|
+ default: errorMsg = "未知错误(代码: " + String(errorCode) + ")";
|
|
|
+ }
|
|
|
+
|
|
|
+ _connectionStatus = "连接失败: " + errorMsg;
|
|
|
+ Serial.println(_connectionStatus);
|
|
|
+ Serial.printf("===== [DEBUG] 连接 '%s' 失败 =====\n", ssid);
|
|
|
+
|
|
|
+ _connectionCompleted = true;
|
|
|
+ _connectionInProgress = false;
|
|
|
+ _apTimeout = millis();
|
|
|
+
|
|
|
+ if (!(WiFi.getMode() & WIFI_AP)) {
|
|
|
+ Serial.println("[DEBUG] 意外退出AP模式,重新启动...");
|
|
|
+ WiFi.mode(WIFI_AP_STA);
|
|
|
+ delay(200);
|
|
|
+ _server.begin();
|
|
|
+ _dnsServer.start(_dnsPort, "*", _apIP);
|
|
|
+ }
|
|
|
+ _server.handleClient();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// HTTP请求处理函数实现
|
|
|
+void WiFiConfigurator::handleRoot() {
|
|
|
+ // 每次访问主页都重置AP超时
|
|
|
+ _instance->_apTimeout = millis();
|
|
|
+
|
|
|
+ String html = _instance->_index_html;
|
|
|
+ String messageBox = "";
|
|
|
+
|
|
|
+ // 显示最后一次连接结果
|
|
|
+ if (_instance->_connectionCompleted) {
|
|
|
+ if (_instance->_connectionSuccess) {
|
|
|
+ messageBox = "<div class='message success'>" + _instance->_connectionStatus + "</div>";
|
|
|
+ } else if (_instance->_connectionStatus.length() > 0) {
|
|
|
+ messageBox = "<div class='message error'>" + _instance->_connectionStatus + "</div>";
|
|
|
+ }
|
|
|
+ // 重置连接完成状态,只显示一次
|
|
|
+ _instance->_connectionCompleted = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用缓存的WiFi扫描结果
|
|
|
+ html.replace("%MESSAGE_BOX%", messageBox);
|
|
|
+ html.replace("%WIFI_LIST%", _instance->_wifiScanResults);
|
|
|
+ html.replace("%AP_SSID%", _instance->_ap_ssid);
|
|
|
+
|
|
|
+ // 发送响应
|
|
|
+ _instance->_server.send(200, "text/html; charset=UTF-8", html);
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleRefresh() {
|
|
|
+ Serial.println("用户请求刷新WiFi列表");
|
|
|
+ _instance->scanWiFiNetworks(); // 重新扫描WiFi
|
|
|
+
|
|
|
+ // 重置AP超时
|
|
|
+ _instance->_apTimeout = millis();
|
|
|
+
|
|
|
+ handleRoot(); // 刷新后显示主页
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleConnect() {
|
|
|
+ // 检查是否有必要的参数
|
|
|
+ if (!_instance->_server.hasArg("ssid") || !_instance->_server.hasArg("password")) {
|
|
|
+ _instance->_connectionStatus = "参数错误:请填写WiFi名称和密码";
|
|
|
+ Serial.println(_instance->_connectionStatus);
|
|
|
+ _instance->_server.sendHeader("Location", "/error");
|
|
|
+ _instance->_server.send(302, "text/plain", "");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String ssid = _instance->_server.arg("ssid");
|
|
|
+ String password = _instance->_server.arg("password");
|
|
|
+ Serial.printf("收到连接请求: SSID=%s, 密码长度=%d\n", ssid.c_str(), password.length());
|
|
|
+
|
|
|
+ // 重置AP超时计时器
|
|
|
+ _instance->_apTimeout = millis();
|
|
|
+ Serial.println("用户点击连接,重置AP超时计时器");
|
|
|
+
|
|
|
+ // 立即响应,告知客户端开始连接
|
|
|
+ _instance->_server.sendHeader("Connection", "close");
|
|
|
+ _instance->_server.send(200, "text/plain", "连接开始");
|
|
|
+
|
|
|
+ // 在后台进行连接操作
|
|
|
+ _instance->connectToWiFi(ssid.c_str(), password.c_str());
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleStatus() {
|
|
|
+ // 每次状态查询都重置AP超时
|
|
|
+ _instance->_apTimeout = millis();
|
|
|
+
|
|
|
+ // 构建JSON响应
|
|
|
+ String json = "{";
|
|
|
+ json += "\"connecting\":" + String(_instance->_connectionInProgress ? "true" : "false") + ",";
|
|
|
+ json += "\"success\":" + String(_instance->_connectionSuccess ? "true" : "false") + ",";
|
|
|
+ json += "\"progress\":" + String(_instance->_connectionProgress) + ",";
|
|
|
+ json += "\"status\":\"" + _instance->_connectionStatus + "\"";
|
|
|
+ json += "}";
|
|
|
+
|
|
|
+ _instance->_server.send(200, "application/json", json);
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleSuccess() {
|
|
|
+ String html = _instance->_success_html;
|
|
|
+ html.replace("%SSID%", _instance->_currentSsid);
|
|
|
+ html.replace("%IP_ADDRESS%", WiFi.localIP().toString());
|
|
|
+ _instance->_server.send(200, "text/html; charset=UTF-8", html);
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleError() {
|
|
|
+ String html = _instance->_error_html;
|
|
|
+ html.replace("%SSID%", _instance->_currentSsid);
|
|
|
+
|
|
|
+ // 生成更具体的错误信息
|
|
|
+ String errorMsg = "请检查:";
|
|
|
+ errorMsg += "<br>- WiFi密码是否正确";
|
|
|
+ errorMsg += "<br>- 路由器信号是否稳定";
|
|
|
+ errorMsg += "<br>- 网络是否允许新设备接入";
|
|
|
+
|
|
|
+ html.replace("%ERROR_MESSAGE%", errorMsg);
|
|
|
+ _instance->_server.send(200, "text/html; charset=UTF-8", html);
|
|
|
+}
|
|
|
+
|
|
|
+void WiFiConfigurator::handleNotFound() {
|
|
|
+ _instance->_server.sendHeader("Location", "/");
|
|
|
+ _instance->_server.send(302, "text/plain", "");
|
|
|
+}
|