WiFiConfigurator.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. #include "WiFiConfigurator.h"
  2. // 静态实例指针初始化
  3. WiFiConfigurator* WiFiConfigurator::_instance = nullptr;
  4. // HTML页面内容
  5. const char captive_html[] PROGMEM = R"rawliteral(
  6. <!DOCTYPE HTML>
  7. <html>
  8. <head>
  9. <meta charset="UTF-8">
  10. <meta http-equiv="refresh" content="0;url=/">
  11. </head>
  12. <body>
  13. </body>
  14. </html>
  15. )rawliteral";
  16. const char index_html[] PROGMEM = R"rawliteral(
  17. <!DOCTYPE HTML>
  18. <html>
  19. <head>
  20. <meta charset="UTF-8">
  21. <title>WiFi配置</title>
  22. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  23. <style>
  24. * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
  25. body { background-color: #f5f5f7; color: #333; padding: 20px; }
  26. .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); }
  27. h1 { color: #1d1d1f; text-align: center; margin-bottom: 25px; font-weight: 600; }
  28. .form-group { margin-bottom: 20px; }
  29. .wifi-selector { display: flex; gap: 10px; align-items: flex-end; }
  30. label { display: block; margin-bottom: 8px; font-weight: 500; color: #6e6e73; }
  31. select, input { width: 100%; padding: 14px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 16px; }
  32. select:focus, input:focus { outline: none; border-color: #0071e3; box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.2); }
  33. button { background-color: #0071e3; color: white; border: none; cursor: pointer; font-weight: 500; padding: 14px; border-radius: 10px; margin-top: 10px; }
  34. button:hover { background-color: #0077ed; }
  35. button:disabled { background-color: #94bfff; cursor: not-allowed; }
  36. .refresh-btn { padding: 14px; width: auto; white-space: nowrap; }
  37. .connect-btn { width: 100%; }
  38. .signal { display: inline-block; width: 80px; text-align: right; color: #86868b; }
  39. .status-bar { text-align: center; margin-top: 20px; color: #6e6e73; font-size: 14px; }
  40. .message { padding: 15px; margin: 15px 0; border-radius: 8px; text-align: center; }
  41. .success { background-color: #eaf8ed; color: #147d39; }
  42. .error { background-color: #fee; color: #c51e24; }
  43. .loading { background-color: #f0f7ff; color: #0066cc; }
  44. .password-hint { font-size: 12px; color: #86868b; margin-top: 5px; }
  45. .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; }
  46. @keyframes spin { to { transform: rotate(360deg); } }
  47. .connection-status { margin-top: 15px; padding: 10px; border-radius: 8px; background-color: #f5f5f7; text-align: center; font-size: 14px; }
  48. .progress-container { margin-top: 10px; background-color: #eaeaea; border-radius: 10px; overflow: hidden; }
  49. .progress-bar { height: 8px; background-color: #0071e3; width: 0%; transition: width 0.3s ease; }
  50. .timeout-warning { color: #ff9500; font-size: 12px; text-align: center; margin-top: 10px; }
  51. </style>
  52. <script>
  53. let checkStatusInterval;
  54. let timeoutTimer;
  55. let remainingTime = 120; // 120秒超时
  56. // 更新超时提示
  57. function updateTimeoutDisplay() {
  58. const element = document.getElementById('timeoutWarning');
  59. if (element) {
  60. element.textContent = `配置热点将在 ${remainingTime} 秒后自动关闭`;
  61. }
  62. if (remainingTime <= 0) {
  63. clearInterval(timeoutTimer);
  64. return;
  65. }
  66. remainingTime--;
  67. }
  68. function startCheckingStatus() {
  69. // 禁用表单防止重复提交
  70. document.getElementById('connectBtn').disabled = true;
  71. document.getElementById('ssid').disabled = true;
  72. document.getElementById('password').disabled = true;
  73. document.querySelector('.refresh-btn').disabled = true;
  74. // 重置超时时间
  75. resetTimeout();
  76. // 显示初始状态
  77. updateStatus({
  78. connecting: true,
  79. status: "准备连接...",
  80. progress: 0
  81. });
  82. // 每200ms检查一次状态
  83. checkStatusInterval = setInterval(() => {
  84. fetch('/status')
  85. .then(response => {
  86. if (!response.ok) throw new Error('网络错误');
  87. return response.json();
  88. })
  89. .then(data => {
  90. updateStatus(data);
  91. // 如果连接完成,停止检查
  92. if (!data.connecting) {
  93. clearInterval(checkStatusInterval);
  94. // 连接成功时显示提示并跳转
  95. if (data.success) {
  96. setTimeout(() => {
  97. window.location.href = '/success';
  98. }, 1000);
  99. } else {
  100. // 连接失败时重新启用表单,允许重试
  101. enableForm();
  102. }
  103. }
  104. })
  105. .catch(err => {
  106. console.error('获取状态失败:', err);
  107. updateStatus({
  108. connecting: false,
  109. status: "通信错误,请重试",
  110. progress: 0
  111. });
  112. clearInterval(checkStatusInterval);
  113. enableForm();
  114. });
  115. }, 200);
  116. }
  117. function updateStatus(data) {
  118. const statusElement = document.getElementById('connectionStatus');
  119. const progressBar = document.getElementById('progressBar');
  120. statusElement.innerHTML = data.connecting ?
  121. '<span class="spinner"></span>' + data.status : data.status;
  122. statusElement.className = 'connection-status ' +
  123. (data.connecting ? 'loading' : (data.success ? 'success' : 'error'));
  124. progressBar.style.width = data.progress + '%';
  125. }
  126. function enableForm() {
  127. // 确保表单元素可操作
  128. document.getElementById('connectBtn').disabled = false;
  129. document.getElementById('ssid').disabled = false;
  130. document.getElementById('password').disabled = false;
  131. document.querySelector('.refresh-btn').disabled = false;
  132. // 聚焦到密码输入框
  133. document.getElementById('password').focus();
  134. }
  135. // 重置超时计时器
  136. function resetTimeout() {
  137. remainingTime = 120;
  138. clearInterval(timeoutTimer);
  139. timeoutTimer = setInterval(updateTimeoutDisplay, 1000);
  140. updateTimeoutDisplay();
  141. }
  142. // 密码验证
  143. function validatePassword() {
  144. const password = document.getElementById('password').value;
  145. const hintElement = document.getElementById('passwordHint');
  146. if (password.length === 0) {
  147. hintElement.textContent = '请输入WiFi密码';
  148. hintElement.style.color = '#86868b';
  149. return true;
  150. } else if (password.length < 8 && password.length > 0) {
  151. hintElement.textContent = '警告: 大多数WiFi密码至少需要8个字符';
  152. hintElement.style.color = '#ff9500';
  153. return false;
  154. } else {
  155. hintElement.textContent = '';
  156. return true;
  157. }
  158. }
  159. window.onload = function() {
  160. // 初始化超时显示
  161. const timeoutDiv = document.createElement('div');
  162. timeoutDiv.id = 'timeoutWarning';
  163. timeoutDiv.className = 'timeout-warning';
  164. document.querySelector('.container').appendChild(timeoutDiv);
  165. resetTimeout();
  166. const passwordInput = document.getElementById('password');
  167. passwordInput.addEventListener('input', validatePassword);
  168. const connectForm = document.getElementById('connectForm');
  169. connectForm.addEventListener('submit', function(e) {
  170. e.preventDefault();
  171. if (validatePassword()) {
  172. // 提交表单数据到后端
  173. const formData = new FormData(connectForm);
  174. fetch('/connect', {
  175. method: 'POST',
  176. body: formData
  177. }).then(() => {
  178. startCheckingStatus();
  179. });
  180. }
  181. });
  182. // 为刷新按钮添加超时重置
  183. document.querySelector('.refresh-btn').addEventListener('click', function() {
  184. resetTimeout();
  185. });
  186. }
  187. </script>
  188. </head>
  189. <body>
  190. <div class="container">
  191. <h1>WiFi网络配置</h1>
  192. %MESSAGE_BOX%
  193. <form id="connectForm">
  194. <div class="form-group">
  195. <label for="ssid">选择WiFi网络:</label>
  196. <div class="wifi-selector">
  197. <select id="ssid" name="ssid" required style="flex: 1;">
  198. %WIFI_LIST%
  199. </select>
  200. <button type="button" class="refresh-btn" onclick="window.location.href='/refresh'">
  201. 刷新
  202. </button>
  203. </div>
  204. </div>
  205. <div class="form-group">
  206. <label for="password">WiFi密码:</label>
  207. <input type="password" id="password" name="password" placeholder="请输入WiFi密码">
  208. <div id="passwordHint" class="password-hint"></div>
  209. </div>
  210. <button type="submit" id="connectBtn" class="connect-btn">
  211. 连接网络
  212. </button>
  213. <div id="connectionStatus" class="connection-status">
  214. 等待操作...
  215. </div>
  216. <div class="progress-container">
  217. <div id="progressBar" class="progress-bar"></div>
  218. </div>
  219. </form>
  220. <div class="status-bar">
  221. 设备热点: %AP_SSID%
  222. </div>
  223. </div>
  224. </body>
  225. </html>
  226. )rawliteral";
  227. const char success_html[] PROGMEM = R"rawliteral(
  228. <!DOCTYPE HTML>
  229. <html>
  230. <head>
  231. <meta charset="UTF-8">
  232. <title>连接成功</title>
  233. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  234. <style>
  235. * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
  236. body { background-color: #f5f5f7; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; color: #333; }
  237. .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); }
  238. h1 { margin-bottom: 20px; font-weight: 600; color: #147d39; }
  239. .message { margin: 20px 0; line-height: 1.6; color: #6e6e73; }
  240. .countdown { color: #0071e3; font-weight: 600; margin: 15px 0; }
  241. </style>
  242. <script>
  243. // 5秒后提示用户切换网络
  244. let countdown = 5;
  245. function updateCountdown() {
  246. document.getElementById('countdown').textContent = countdown;
  247. if (countdown <= 0) {
  248. document.getElementById('countdownMessage').textContent =
  249. '请切换到已配置的WiFi网络';
  250. return;
  251. }
  252. countdown--;
  253. setTimeout(updateCountdown, 1000);
  254. }
  255. window.onload = updateCountdown;
  256. </script>
  257. </head>
  258. <body>
  259. <div class="container">
  260. <h1>连接成功!</h1>
  261. <div class="message">
  262. <p>已成功连接到 %SSID%</p>
  263. <p>IP地址: %IP_ADDRESS%</p>
  264. <div id="countdownMessage" class="countdown">
  265. 配置热点将在 <span id="countdown">5</span> 秒后关闭
  266. </div>
  267. </div>
  268. </div>
  269. </body>
  270. </html>
  271. )rawliteral";
  272. const char error_html[] PROGMEM = R"rawliteral(
  273. <!DOCTYPE HTML>
  274. <html>
  275. <head>
  276. <meta charset="UTF-8">
  277. <title>连接失败</title>
  278. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  279. <style>
  280. * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
  281. body { background-color: #f5f5f7; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; color: #333; }
  282. .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); }
  283. h1 { margin-bottom: 20px; font-weight: 600; color: #c51e24; }
  284. .message { margin: 20px 0; line-height: 1.6; color: #6e6e73; }
  285. 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; }
  286. button:hover { background-color: #0077ed; }
  287. </style>
  288. </head>
  289. <body>
  290. <div class="container">
  291. <h1>连接失败</h1>
  292. <div class="message">
  293. <p>无法连接到 %SSID%</p>
  294. <p>%ERROR_MESSAGE%</p>
  295. </div>
  296. <button onclick="window.location.href='/'">返回重试</button>
  297. </div>
  298. </body>
  299. </html>
  300. )rawliteral";
  301. // 构造函数
  302. WiFiConfigurator::WiFiConfigurator(int slowLedPin, int fastLedPin, int configButtonPin,
  303. const char* ap_ssid, const char* ap_password,
  304. IPAddress apIP, IPAddress gateway, IPAddress subnet)
  305. : _slowLedPin(slowLedPin),
  306. _fastLedPin(fastLedPin),
  307. _configButtonPin(configButtonPin),
  308. _ap_ssid(ap_ssid),
  309. _ap_password(ap_password),
  310. _apIP(apIP),
  311. _gateway(gateway),
  312. _subnet(subnet),
  313. _server(80),
  314. _captive_html(captive_html),
  315. _index_html(index_html),
  316. _success_html(success_html),
  317. _error_html(error_html) {
  318. _instance = this;
  319. }
  320. // 初始化函数
  321. void WiFiConfigurator::begin() {
  322. // 初始化LED
  323. pinMode(_slowLedPin, OUTPUT);
  324. pinMode(_fastLedPin, OUTPUT);
  325. // 初始化按键,使用内部上拉电阻
  326. pinMode(_configButtonPin, INPUT_PULLUP);
  327. Serial.println("WiFi配置器初始化...");
  328. _preferences.begin("wifi-config", false);
  329. // 启动时立即扫描WiFi
  330. scanWiFiNetworks();
  331. // 尝试连接已保存的WiFi
  332. String savedSsid = _preferences.getString("ssid", "");
  333. String savedPassword = _preferences.getString("password", "");
  334. if (savedSsid.length() > 0) {
  335. Serial.println("尝试连接已保存的WiFi...");
  336. connectToWiFi(savedSsid.c_str(), savedPassword.c_str());
  337. } else {
  338. startAPMode();
  339. }
  340. }
  341. // 主循环处理函数
  342. void WiFiConfigurator::loop() {
  343. // 处理按键输入
  344. handleConfigButton();
  345. // LED控制
  346. unsigned long currentMillis = millis();
  347. // 慢闪LED
  348. if (currentMillis - _previousSlowMillis >= _slowInterval) {
  349. _previousSlowMillis = currentMillis;
  350. _slowLedState = !_slowLedState;
  351. digitalWrite(_slowLedPin, _slowLedState);
  352. }
  353. // 快闪LED - 连接过程中加速闪烁
  354. if (_connectionInProgress) {
  355. if (currentMillis - _previousFastMillis >= 100) { // 加速闪烁
  356. _previousFastMillis = currentMillis;
  357. _fastLedState = !_fastLedState;
  358. digitalWrite(_fastLedPin, _fastLedState);
  359. }
  360. } else if (WiFi.getMode() == WIFI_AP || WiFi.status() != WL_CONNECTED) {
  361. if (currentMillis - _previousFastMillis >= _fastInterval) {
  362. _previousFastMillis = currentMillis;
  363. _fastLedState = !_fastLedState;
  364. digitalWrite(_fastLedPin, _fastLedState);
  365. }
  366. } else {
  367. digitalWrite(_fastLedPin, HIGH);
  368. }
  369. // 持续处理网络请求
  370. if (WiFi.getMode() & WIFI_AP) { // 只要AP模式开启就处理请求
  371. _dnsServer.processNextRequest();
  372. _server.handleClient();
  373. // 检查AP模式超时(仅在连接未进行时)
  374. if (!_connectionInProgress && _apTimeout > 0 && millis() - _apTimeout > _apTimeoutDuration) {
  375. Serial.println("AP模式超时,关闭热点");
  376. WiFi.softAPdisconnect(true);
  377. _server.stop();
  378. _dnsServer.stop();
  379. _apTimeout = 0;
  380. }
  381. }
  382. // 连接成功后保持AP模式一段时间
  383. if (_connectionSuccess && (WiFi.getMode() & WIFI_AP) &&
  384. millis() - _successNotificationTime > _successNotificationDelay) {
  385. Serial.println("成功通知时间已到,关闭AP模式");
  386. WiFi.softAPdisconnect(true);
  387. _server.stop();
  388. _dnsServer.stop();
  389. _apTimeout = 0;
  390. }
  391. }
  392. // 检查是否已连接WiFi
  393. bool WiFiConfigurator::isConnected() {
  394. return WiFi.status() == WL_CONNECTED;
  395. }
  396. // 获取当前连接的SSID
  397. String WiFiConfigurator::getCurrentSSID() {
  398. return WiFi.status() == WL_CONNECTED ? WiFi.SSID() : "";
  399. }
  400. // 获取IP地址
  401. String WiFiConfigurator::getIPAddress() {
  402. return WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "";
  403. }
  404. // 处理配置按键
  405. void WiFiConfigurator::handleConfigButton() {
  406. // 读取按键状态(LOW表示按下,因为使用了内部上拉)
  407. int buttonState = digitalRead(_configButtonPin);
  408. // static bool slowLedAlwaysOn = false; // 标记慢闪灯是否已常亮
  409. // 按键按下
  410. if (buttonState == LOW && !_buttonPressed) {
  411. _buttonPressed = true;
  412. _buttonPressStartTime = millis();
  413. // slowLedAlwaysOn = false; // 重置状态
  414. Serial.println("配置按键被按下...");
  415. // 开始快速闪烁LED,表示正在计时
  416. _fastLedState = HIGH;
  417. digitalWrite(_fastLedPin, _fastLedState);
  418. }
  419. // 按键释放
  420. else if (buttonState == HIGH && _buttonPressed) {
  421. unsigned long pressDuration = millis() - _buttonPressStartTime;
  422. _buttonPressed = false;
  423. // slowLedAlwaysOn = false; // 重置状态
  424. // 检查按下时间是否达到要求
  425. if (pressDuration >= _configButtonPressDuration) {
  426. Serial.println("按键长按5秒以上,进入配网模式...");
  427. enterConfigMode();
  428. } else {
  429. Serial.printf("按键释放,按下时间: %d毫秒(不足5秒)\n", pressDuration);
  430. }
  431. }
  432. // 按键持续按下中
  433. else if (buttonState == LOW && _buttonPressed) {
  434. unsigned long pressDuration = millis() - _buttonPressStartTime;
  435. // 每100ms检查一次(提高响应速度)
  436. if (pressDuration % 100 < 10) {
  437. // 计算已按下的秒数
  438. int secondsPressed = pressDuration / 1000;
  439. Serial.printf("按键持续按下中: %d秒/%d秒\n", secondsPressed, _configButtonPressDuration / 1000);
  440. // 快闪LED提示用户还在计时
  441. _fastLedState = !_fastLedState;
  442. digitalWrite(_fastLedPin, _fastLedState);
  443. // 关键修改:按住期间达到5秒且尚未设置常亮
  444. if (pressDuration >= _configButtonPressDuration) {
  445. // if (pressDuration >= _configButtonPressDuration && !slowLedAlwaysOn) {
  446. Serial.println("已按住5秒,慢闪灯常亮");
  447. _slowLedState = HIGH; // 设置慢闪LED状态为常亮
  448. digitalWrite(_slowLedPin, _slowLedState);
  449. // slowLedAlwaysOn = true; // 标记已常亮,避免重复设置
  450. }
  451. }
  452. }
  453. }
  454. // 进入配网模式
  455. void WiFiConfigurator::enterConfigMode() {
  456. // 清除已保存的WiFi配置
  457. _preferences.putString("ssid", "");
  458. _preferences.putString("password", "");
  459. // 停止当前的WiFi连接
  460. WiFi.disconnect();
  461. // 重新启动AP模式和Web服务
  462. startAPMode();
  463. // 闪烁LED提示进入配网模式
  464. for (int i = 0; i < 5; i++) {
  465. digitalWrite(_slowLedPin, HIGH);
  466. digitalWrite(_fastLedPin, HIGH);
  467. delay(200);
  468. digitalWrite(_slowLedPin, LOW);
  469. digitalWrite(_fastLedPin, LOW);
  470. delay(200);
  471. }
  472. Serial.println("已进入配网模式,请连接设备热点进行配置");
  473. }
  474. // 扫描WiFi网络并缓存结果
  475. void WiFiConfigurator::scanWiFiNetworks() {
  476. Serial.println("开始扫描WiFi网络...");
  477. int n = WiFi.scanNetworks(false, false, false, 100);
  478. Serial.printf("发现 %d 个WiFi网络\n", n);
  479. _wifiScanResults = "";
  480. for (int i = 0; i < n; i++) {
  481. int signalStrength = map(WiFi.RSSI(i), -100, -50, 0, 4);
  482. String signalBar = "";
  483. for(int j = 0; j < signalStrength; j++) signalBar += "●";
  484. for(int j = signalStrength; j < 4; j++) signalBar += "○";
  485. _wifiScanResults += "<option value=\"" + WiFi.SSID(i) + "\">" +
  486. WiFi.SSID(i) + " <span class=\"signal\">" + signalBar + "</span></option>";
  487. // 串口打印扫描结果
  488. Serial.printf("网络: %s, 信号强度: %d, 加密方式: %d\n",
  489. WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.encryptionType(i));
  490. }
  491. WiFi.scanDelete();
  492. }
  493. void WiFiConfigurator::startAPMode() {
  494. Serial.println("启动AP模式...");
  495. // 重置连接状态
  496. _connectionInProgress = false;
  497. _connectionCompleted = false;
  498. _connectionSuccess = false;
  499. _connectionStatus = "";
  500. _connectionProgress = 0;
  501. // 配置为AP模式
  502. WiFi.mode(WIFI_AP);
  503. delay(200); // 增加延迟确保模式切换完成
  504. bool configSuccess = WiFi.softAPConfig(_apIP, _gateway, _subnet);
  505. Serial.print("AP配置: ");
  506. Serial.println(configSuccess ? "成功" : "失败");
  507. // 启动AP
  508. bool apStarted = WiFi.softAP(_ap_ssid, _ap_password, 1, false, 4);
  509. if (!apStarted) {
  510. Serial.println("AP启动失败,尝试默认配置...");
  511. apStarted = WiFi.softAP(_ap_ssid, _ap_password);
  512. }
  513. Serial.print("AP启动: ");
  514. Serial.println(apStarted ? "成功" : "失败");
  515. // 配置DNS
  516. _dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
  517. bool dnsStarted = _dnsServer.start(_dnsPort, "*", _apIP);
  518. Serial.print("DNS服务器启动: ");
  519. Serial.println(dnsStarted ? "成功" : "失败");
  520. // 苹果Captive Portal支持
  521. _server.on("/hotspot-detect.html", [](){
  522. _instance->_server.send(200, "text/html", _instance->_captive_html);
  523. });
  524. _server.on("/library/test/success.html", [](){
  525. _instance->_server.send(200, "text/html", _instance->_captive_html);
  526. });
  527. // 主要路由
  528. _server.on("/", handleRoot);
  529. _server.on("/refresh", handleRefresh);
  530. _server.on("/connect", HTTP_POST, handleConnect);
  531. _server.on("/status", handleStatus);
  532. _server.on("/success", handleSuccess);
  533. _server.on("/error", handleError);
  534. _server.onNotFound(handleNotFound);
  535. _server.begin();
  536. Serial.print("AP IP地址: ");
  537. Serial.println(WiFi.softAPIP());
  538. // 重置AP超时计时器
  539. _apTimeout = millis();
  540. }
  541. void WiFiConfigurator::connectToWiFi(const char* ssid, const char* password) {
  542. _currentSsid = ssid;
  543. _connectionInProgress = true;
  544. _connectionCompleted = false;
  545. _connectionSuccess = false;
  546. _connectionProgress = 0;
  547. Serial.println("\n===== [DEBUG] 开始连接WiFi流程 =====\n");
  548. // 完全断开所有WiFi连接
  549. Serial.println("[DEBUG] 1. 正在清除之前的WiFi连接信息...");
  550. WiFi.disconnect(true);
  551. delay(500); // 增加一点延迟,确保清除操作完成
  552. // 配置为AP+STA模式
  553. Serial.println("[DEBUG] 2. 设置WiFi模式为 AP+STA...");
  554. WiFi.mode(WIFI_AP_STA);
  555. delay(200);
  556. // 打印将要使用的SSID和密码
  557. Serial.print("[DEBUG] 3. 准备连接到网络...");
  558. Serial.print(" SSID: '");
  559. Serial.print(ssid);
  560. Serial.print("' ");
  561. Serial.print(" Password: '");
  562. Serial.print(password);
  563. Serial.println("'");
  564. // 开始连接
  565. Serial.println("[DEBUG] 4. 调用 WiFi.begin()...");
  566. WiFi.begin(ssid, password);
  567. // 等待连接结果(最多20秒)
  568. Serial.println("[DEBUG] 5. 开始等待连接结果...");
  569. unsigned long startAttemptTime = millis();
  570. while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 20000) {
  571. // 打印当前状态码
  572. Serial.printf("[DEBUG] 状态码: %d (等待连接中...)\n", WiFi.status());
  573. int elapsedSeconds = (millis() - startAttemptTime) / 1000;
  574. _connectionProgress = 20 + (elapsedSeconds * 70) / 20;
  575. if (_connectionProgress > 90) _connectionProgress = 90;
  576. _dnsServer.processNextRequest();
  577. _server.handleClient();
  578. delay(1000);
  579. }
  580. // 检查最终连接结果
  581. Serial.println("\n[DEBUG] 6. 连接尝试结束,检查最终结果...");
  582. if (WiFi.status() == WL_CONNECTED) {
  583. _connectionSuccess = true;
  584. String ipAddress = WiFi.localIP().toString();
  585. _connectionStatus = "成功连接到 " + _currentSsid + ",IP地址: " + ipAddress;
  586. Serial.println("[DEBUG] => 连接成功!");
  587. Serial.printf("===== [DEBUG] 成功连接到 '%s',IP: %s =====\n", ssid, ipAddress.c_str());
  588. _preferences.putString("ssid", ssid);
  589. _preferences.putString("password", password);
  590. _connectionCompleted = true;
  591. _connectionInProgress = false;
  592. _connectionProgress = 100; // 标记进度为100%
  593. // 不立即关闭AP,而是记录成功时间,稍后在loop中关闭
  594. _successNotificationTime = millis();
  595. Serial.println("连接成功,保持AP模式5秒以确保客户端收到通知");
  596. } else {
  597. _connectionSuccess = false;
  598. int errorCode = WiFi.status();
  599. Serial.println("[DEBUG] => 连接失败!");
  600. Serial.printf("[DEBUG] 最终状态码: %d\n", errorCode);
  601. String errorMsg;
  602. switch(errorCode) {
  603. case WL_CONNECT_FAILED: errorMsg = "连接失败(密码错误或网络拒绝接入)"; break;
  604. case WL_CONNECTION_LOST: errorMsg = "连接丢失"; break;
  605. case WL_DISCONNECTED: errorMsg = "已断开连接"; break;
  606. default: errorMsg = "未知错误(代码: " + String(errorCode) + ")";
  607. }
  608. _connectionStatus = "连接失败: " + errorMsg;
  609. Serial.println(_connectionStatus);
  610. Serial.printf("===== [DEBUG] 连接 '%s' 失败 =====\n", ssid);
  611. _connectionCompleted = true;
  612. _connectionInProgress = false;
  613. _apTimeout = millis();
  614. if (!(WiFi.getMode() & WIFI_AP)) {
  615. Serial.println("[DEBUG] 意外退出AP模式,重新启动...");
  616. WiFi.mode(WIFI_AP_STA);
  617. delay(200);
  618. _server.begin();
  619. _dnsServer.start(_dnsPort, "*", _apIP);
  620. }
  621. _server.handleClient();
  622. }
  623. }
  624. // HTTP请求处理函数实现
  625. void WiFiConfigurator::handleRoot() {
  626. // 每次访问主页都重置AP超时
  627. _instance->_apTimeout = millis();
  628. String html = _instance->_index_html;
  629. String messageBox = "";
  630. // 显示最后一次连接结果
  631. if (_instance->_connectionCompleted) {
  632. if (_instance->_connectionSuccess) {
  633. messageBox = "<div class='message success'>" + _instance->_connectionStatus + "</div>";
  634. } else if (_instance->_connectionStatus.length() > 0) {
  635. messageBox = "<div class='message error'>" + _instance->_connectionStatus + "</div>";
  636. }
  637. // 重置连接完成状态,只显示一次
  638. _instance->_connectionCompleted = false;
  639. }
  640. // 使用缓存的WiFi扫描结果
  641. html.replace("%MESSAGE_BOX%", messageBox);
  642. html.replace("%WIFI_LIST%", _instance->_wifiScanResults);
  643. html.replace("%AP_SSID%", _instance->_ap_ssid);
  644. // 发送响应
  645. _instance->_server.send(200, "text/html; charset=UTF-8", html);
  646. }
  647. void WiFiConfigurator::handleRefresh() {
  648. Serial.println("用户请求刷新WiFi列表");
  649. _instance->scanWiFiNetworks(); // 重新扫描WiFi
  650. // 重置AP超时
  651. _instance->_apTimeout = millis();
  652. handleRoot(); // 刷新后显示主页
  653. }
  654. void WiFiConfigurator::handleConnect() {
  655. // 检查是否有必要的参数
  656. if (!_instance->_server.hasArg("ssid") || !_instance->_server.hasArg("password")) {
  657. _instance->_connectionStatus = "参数错误:请填写WiFi名称和密码";
  658. Serial.println(_instance->_connectionStatus);
  659. _instance->_server.sendHeader("Location", "/error");
  660. _instance->_server.send(302, "text/plain", "");
  661. return;
  662. }
  663. String ssid = _instance->_server.arg("ssid");
  664. String password = _instance->_server.arg("password");
  665. Serial.printf("收到连接请求: SSID=%s, 密码长度=%d\n", ssid.c_str(), password.length());
  666. // 重置AP超时计时器
  667. _instance->_apTimeout = millis();
  668. Serial.println("用户点击连接,重置AP超时计时器");
  669. // 立即响应,告知客户端开始连接
  670. _instance->_server.sendHeader("Connection", "close");
  671. _instance->_server.send(200, "text/plain", "连接开始");
  672. // 在后台进行连接操作
  673. _instance->connectToWiFi(ssid.c_str(), password.c_str());
  674. }
  675. void WiFiConfigurator::handleStatus() {
  676. // 每次状态查询都重置AP超时
  677. _instance->_apTimeout = millis();
  678. // 构建JSON响应
  679. String json = "{";
  680. json += "\"connecting\":" + String(_instance->_connectionInProgress ? "true" : "false") + ",";
  681. json += "\"success\":" + String(_instance->_connectionSuccess ? "true" : "false") + ",";
  682. json += "\"progress\":" + String(_instance->_connectionProgress) + ",";
  683. json += "\"status\":\"" + _instance->_connectionStatus + "\"";
  684. json += "}";
  685. _instance->_server.send(200, "application/json", json);
  686. }
  687. void WiFiConfigurator::handleSuccess() {
  688. String html = _instance->_success_html;
  689. html.replace("%SSID%", _instance->_currentSsid);
  690. html.replace("%IP_ADDRESS%", WiFi.localIP().toString());
  691. _instance->_server.send(200, "text/html; charset=UTF-8", html);
  692. }
  693. void WiFiConfigurator::handleError() {
  694. String html = _instance->_error_html;
  695. html.replace("%SSID%", _instance->_currentSsid);
  696. // 生成更具体的错误信息
  697. String errorMsg = "请检查:";
  698. errorMsg += "<br>- WiFi密码是否正确";
  699. errorMsg += "<br>- 路由器信号是否稳定";
  700. errorMsg += "<br>- 网络是否允许新设备接入";
  701. html.replace("%ERROR_MESSAGE%", errorMsg);
  702. _instance->_server.send(200, "text/html; charset=UTF-8", html);
  703. }
  704. void WiFiConfigurator::handleNotFound() {
  705. _instance->_server.sendHeader("Location", "/");
  706. _instance->_server.send(302, "text/plain", "");
  707. }