feat(platform): migrate to softap web provisioning portal
This commit is contained in:
15
README.md
15
README.md
@@ -77,8 +77,10 @@ idf.py build
|
|||||||
## Config
|
## Config
|
||||||
|
|
||||||
In `menuconfig -> TQ Controller Config`:
|
In `menuconfig -> TQ Controller Config`:
|
||||||
- `TQ_WIFI_SSID`
|
- `TQ_WIFI_PROV_SOFTAP_SSID`
|
||||||
- `TQ_WIFI_PASSWORD`
|
- `TQ_WIFI_PROV_SOFTAP_PASSWORD`
|
||||||
|
- `TQ_WIFI_PROV_SOFTAP_CHANNEL`
|
||||||
|
- `TQ_WIFI_PROV_SOFTAP_MAX_CONN`
|
||||||
- `TQ_HTTP_PORT`
|
- `TQ_HTTP_PORT`
|
||||||
- `TQ_API_KEY` (optional)
|
- `TQ_API_KEY` (optional)
|
||||||
- Z-Image HTTP auth and model fields:
|
- Z-Image HTTP auth and model fields:
|
||||||
@@ -123,9 +125,12 @@ When `TQ_API_KEY` is not empty, each request must include:
|
|||||||
X-API-Key: <your-key>
|
X-API-Key: <your-key>
|
||||||
```
|
```
|
||||||
|
|
||||||
STA-only behavior:
|
Wi-Fi provisioning behavior:
|
||||||
- If SSID is empty, startup fails.
|
- Device first tries saved STA credentials from NVS (`wifi_cfg` namespace).
|
||||||
- If STA connect fails, startup fails (no AP fallback).
|
- If no valid credentials exist or STA connect fails, device starts a SoftAP portal.
|
||||||
|
- Connect to the SoftAP and open `http://192.168.4.1`; the static page auto-scans nearby SSIDs for selection.
|
||||||
|
- Provisioning page supports Chinese/English switching and remembers the selected language in browser local storage.
|
||||||
|
- Credentials are written to NVS only after STA connect succeeds.
|
||||||
|
|
||||||
## Flash
|
## Flash
|
||||||
|
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ idf_component_register(
|
|||||||
esp_audio_codec
|
esp_audio_codec
|
||||||
driver
|
driver
|
||||||
EMBED_FILES
|
EMBED_FILES
|
||||||
|
"assets/wifi_configuration.html"
|
||||||
"assets/talkingq_logo.png"
|
"assets/talkingq_logo.png"
|
||||||
)
|
)
|
||||||
|
|||||||
580
components/platform/assets/wifi_configuration.html
Normal file
580
components/platform/assets/wifi_configuration.html
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="referrer no-referrer">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<title>TalkingQ Network Configuration</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-a: #031f4c;
|
||||||
|
--bg-b: #0a66ff;
|
||||||
|
--bg-c: #2bc7ff;
|
||||||
|
--text-primary: #0b1c35;
|
||||||
|
--text-secondary: #4f5f86;
|
||||||
|
--btn-a: #0a61ff;
|
||||||
|
--btn-b: #24befc;
|
||||||
|
--ok-bg: #dcfce7;
|
||||||
|
--ok-text: #166534;
|
||||||
|
--err-bg: #fee2e2;
|
||||||
|
--err-text: #991b1b;
|
||||||
|
--muted-bg: #eef4ff;
|
||||||
|
--muted-text: #2a4b95;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, var(--bg-a) 0%, var(--bg-b) 45%, var(--bg-c) 100%);
|
||||||
|
padding: 28px 14px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border-radius: 26px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 28px 65px rgba(5, 29, 82, 0.35);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 24px 26px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(120deg, #0a54ff, #1f95ff 60%, #66e0ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.55);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
box-shadow: 0 8px 22px rgba(3, 18, 66, 0.25);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
opacity: 0.86;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch {
|
||||||
|
width: 156px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch select:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 28px 24px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0a2d72;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid #e0e8ff;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #f8fbff;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #1a2f5c;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #d6def0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2b8cff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(43, 140, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
background: linear-gradient(120deg, var(--btn-a), var(--btn-b));
|
||||||
|
box-shadow: 0 12px 30px rgba(9, 71, 161, 0.35);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 16px 34px rgba(8, 64, 150, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
background: #b9c6e4;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
color: var(--muted-text);
|
||||||
|
background: var(--muted-bg);
|
||||||
|
border: 1px solid #c9d8ff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 16px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #dce5ff;
|
||||||
|
background: #f4f7ff;
|
||||||
|
color: #334c7d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.ok {
|
||||||
|
background: var(--ok-bg);
|
||||||
|
color: var(--ok-text);
|
||||||
|
border-color: #b7e9c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.err {
|
||||||
|
background: var(--err-bg);
|
||||||
|
color: var(--err-text);
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.brand-header {
|
||||||
|
padding: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-switch {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 22px 18px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-shell">
|
||||||
|
<div class="card">
|
||||||
|
<div class="brand-header">
|
||||||
|
<div class="brand-logo">TQ</div>
|
||||||
|
<div class="brand-copy">
|
||||||
|
<h1>TalkingQ</h1>
|
||||||
|
<p data-i18n="tagline">Draw smarter. Connect faster.</p>
|
||||||
|
</div>
|
||||||
|
<div class="language-switch">
|
||||||
|
<select id="language" aria-label="Language">
|
||||||
|
<option value="zh-CN">简体中文</option>
|
||||||
|
<option value="en-US">English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="intro">
|
||||||
|
<h2 data-i18n="title">Wi-Fi Configuration</h2>
|
||||||
|
<p data-i18n="subtitle">Select a scanned network or enter SSID manually. Credentials are saved only after connection succeeds.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="field">
|
||||||
|
<button id="scan-btn" class="button secondary" type="button" data-i18n="scan_button">Scan Nearby Wi-Fi</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="network-list" data-i18n="nearby_label">Nearby Wi-Fi</label>
|
||||||
|
<select id="network-list">
|
||||||
|
<option value="" data-i18n="manual_option">Manual input</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="wifi-form">
|
||||||
|
<div class="field">
|
||||||
|
<label for="ssid" data-i18n="ssid_label">SSID</label>
|
||||||
|
<input id="ssid" name="ssid" maxlength="32" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="password" data-i18n="password_label">Password</label>
|
||||||
|
<input id="password" name="password" maxlength="64" type="password" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button id="submit-btn" class="button" type="submit" data-i18n="connect_button">Connect</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="status" class="status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('wifi-form');
|
||||||
|
const statusBox = document.getElementById('status');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const scanBtn = document.getElementById('scan-btn');
|
||||||
|
const networkList = document.getElementById('network-list');
|
||||||
|
const ssidInput = document.getElementById('ssid');
|
||||||
|
const languageSelect = document.getElementById('language');
|
||||||
|
|
||||||
|
const i18n = {
|
||||||
|
'zh-CN': {
|
||||||
|
title: 'Wi-Fi 配网',
|
||||||
|
subtitle: '可先从扫描结果选择网络,也可以手动输入 SSID。仅在连接成功后才会保存密码。',
|
||||||
|
tagline: 'Draw smarter. Connect faster.',
|
||||||
|
scan_button: '扫描附近 Wi-Fi',
|
||||||
|
nearby_label: '附近 Wi-Fi',
|
||||||
|
manual_option: '手动输入',
|
||||||
|
ssid_label: 'SSID',
|
||||||
|
password_label: '密码',
|
||||||
|
connect_button: '连接',
|
||||||
|
status_ready: '准备就绪。',
|
||||||
|
status_scanning: '正在扫描附近 Wi-Fi...',
|
||||||
|
status_scan_ok: '扫描完成,请选择网络后连接。',
|
||||||
|
status_scan_empty: '未扫描到 Wi-Fi,请手动输入 SSID。',
|
||||||
|
status_scan_failed: '扫描失败,请手动输入 SSID。',
|
||||||
|
status_ssid_required: 'SSID 不能为空。',
|
||||||
|
status_connecting: '正在连接,请稍候...',
|
||||||
|
status_connect_ok: '连接成功,正在关闭配网页面...',
|
||||||
|
status_connect_failed: '连接失败:{error}。请重试。',
|
||||||
|
status_request_failed: '请求失败,请重试。',
|
||||||
|
signal_unit: 'dBm'
|
||||||
|
},
|
||||||
|
'en-US': {
|
||||||
|
title: 'Wi-Fi Configuration',
|
||||||
|
subtitle: 'Select a scanned network or enter SSID manually. Credentials are saved only after connection succeeds.',
|
||||||
|
tagline: 'Draw smarter. Connect faster.',
|
||||||
|
scan_button: 'Scan Nearby Wi-Fi',
|
||||||
|
nearby_label: 'Nearby Wi-Fi',
|
||||||
|
manual_option: 'Manual input',
|
||||||
|
ssid_label: 'SSID',
|
||||||
|
password_label: 'Password',
|
||||||
|
connect_button: 'Connect',
|
||||||
|
status_ready: 'Ready.',
|
||||||
|
status_scanning: 'Scanning nearby Wi-Fi...',
|
||||||
|
status_scan_ok: 'Scan complete. Select a network and connect.',
|
||||||
|
status_scan_empty: 'No Wi-Fi found. Please input SSID manually.',
|
||||||
|
status_scan_failed: 'Scan failed. Please input SSID manually.',
|
||||||
|
status_ssid_required: 'SSID is required.',
|
||||||
|
status_connecting: 'Connecting, please wait...',
|
||||||
|
status_connect_ok: 'Connected successfully. Closing this provisioning page...',
|
||||||
|
status_connect_failed: 'Connection failed: {error}. Please try again.',
|
||||||
|
status_request_failed: 'Request failed. Please try again.',
|
||||||
|
signal_unit: 'dBm'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageMap = {
|
||||||
|
'zh': 'zh-CN',
|
||||||
|
'zh-CN': 'zh-CN',
|
||||||
|
'zh-SG': 'zh-CN',
|
||||||
|
'en': 'en-US',
|
||||||
|
'en-US': 'en-US',
|
||||||
|
'en-GB': 'en-US',
|
||||||
|
'en-CA': 'en-US',
|
||||||
|
'en-AU': 'en-US'
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentLang = 'zh-CN';
|
||||||
|
|
||||||
|
function tr(key) {
|
||||||
|
const langPack = i18n[currentLang] || i18n['en-US'];
|
||||||
|
return langPack[key] || i18n['en-US'][key] || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trWithParams(key, params) {
|
||||||
|
let text = tr(key);
|
||||||
|
if (!params) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(params).forEach((k) => {
|
||||||
|
text = text.split('{' + k + '}').join(String(params[k]));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatusText(text, type) {
|
||||||
|
statusBox.textContent = text;
|
||||||
|
statusBox.className = type ? ('status ' + type) : 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatusKey(key, type, params) {
|
||||||
|
setStatusText(trWithParams(key, params), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeSsid(value) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch (_err) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProvisionPage() {
|
||||||
|
window.open('', '_self');
|
||||||
|
window.close();
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.replace('about:blank');
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslations() {
|
||||||
|
document.documentElement.lang = currentLang;
|
||||||
|
document.title = 'TalkingQ Network Configuration';
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
const text = tr(key);
|
||||||
|
if (text) {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const manualOption = networkList.querySelector('option[value=\"\"]');
|
||||||
|
if (manualOption) {
|
||||||
|
manualOption.textContent = tr('manual_option');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusBox.textContent) {
|
||||||
|
setStatusKey('status_ready', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLanguage(input) {
|
||||||
|
if (!input) {
|
||||||
|
return 'zh-CN';
|
||||||
|
}
|
||||||
|
if (languageMap[input]) {
|
||||||
|
return languageMap[input];
|
||||||
|
}
|
||||||
|
const main = input.split('-')[0];
|
||||||
|
return languageMap[main] || 'zh-CN';
|
||||||
|
}
|
||||||
|
|
||||||
|
function initLanguage() {
|
||||||
|
const saved = localStorage.getItem('provision_lang');
|
||||||
|
const browser = (navigator.language || 'zh-CN').trim();
|
||||||
|
currentLang = normalizeLanguage(saved || browser);
|
||||||
|
languageSelect.value = currentLang;
|
||||||
|
applyTranslations();
|
||||||
|
}
|
||||||
|
|
||||||
|
languageSelect.addEventListener('change', () => {
|
||||||
|
currentLang = normalizeLanguage(languageSelect.value);
|
||||||
|
localStorage.setItem('provision_lang', currentLang);
|
||||||
|
applyTranslations();
|
||||||
|
});
|
||||||
|
|
||||||
|
function rebuildNetworkList(networks) {
|
||||||
|
networkList.innerHTML = '';
|
||||||
|
const manualOption = document.createElement('option');
|
||||||
|
manualOption.value = '';
|
||||||
|
manualOption.textContent = tr('manual_option');
|
||||||
|
networkList.appendChild(manualOption);
|
||||||
|
|
||||||
|
networks.forEach((item) => {
|
||||||
|
const ssid = decodeSsid(item.ssid || '');
|
||||||
|
if (!ssid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = document.createElement('option');
|
||||||
|
const signal = typeof item.rssi === 'number' ? (item.rssi + ' ' + tr('signal_unit')) : ('? ' + tr('signal_unit'));
|
||||||
|
const auth = item.auth || 'unknown';
|
||||||
|
option.value = ssid;
|
||||||
|
option.textContent = ssid + ' (' + signal + ', ' + auth + ')';
|
||||||
|
networkList.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanNetworks() {
|
||||||
|
scanBtn.disabled = true;
|
||||||
|
setStatusKey('status_scanning', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/scan', { cache: 'no-store' });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
throw new Error(data.error || 'scan_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const networks = Array.isArray(data.networks) ? data.networks : [];
|
||||||
|
rebuildNetworkList(networks);
|
||||||
|
|
||||||
|
if (networks.length > 0) {
|
||||||
|
setStatusKey('status_scan_ok', '');
|
||||||
|
} else {
|
||||||
|
setStatusKey('status_scan_empty', 'err');
|
||||||
|
}
|
||||||
|
} catch (_err) {
|
||||||
|
rebuildNetworkList([]);
|
||||||
|
setStatusKey('status_scan_failed', 'err');
|
||||||
|
}
|
||||||
|
|
||||||
|
scanBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkList.addEventListener('change', () => {
|
||||||
|
if (networkList.value) {
|
||||||
|
ssidInput.value = networkList.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scanBtn.addEventListener('click', () => {
|
||||||
|
scanNetworks();
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let ssid = ssidInput.value.trim();
|
||||||
|
if (!ssid && networkList.value) {
|
||||||
|
ssid = networkList.value;
|
||||||
|
ssidInput.value = ssid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
if (!ssid) {
|
||||||
|
setStatusKey('status_ssid_required', 'err');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
setStatusKey('status_connecting', '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = 'ssid=' + encodeURIComponent(ssid) + '&password=' + encodeURIComponent(password);
|
||||||
|
const response = await fetch('/provision', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: payload
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.ok) {
|
||||||
|
setStatusKey('status_connect_ok', 'ok');
|
||||||
|
setTimeout(() => {
|
||||||
|
closeProvisionPage();
|
||||||
|
}, 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatusKey('status_connect_failed', 'err', { error: data.error || 'unknown' });
|
||||||
|
} catch (_err) {
|
||||||
|
setStatusKey('status_request_failed', 'err');
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
initLanguage();
|
||||||
|
setStatusKey('status_ready', '');
|
||||||
|
scanNetworks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,26 +1,67 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
|
#include "esp_http_server.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "esp_wifi_default.h"
|
#include "esp_wifi_default.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/event_groups.h"
|
#include "freertos/event_groups.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
|
||||||
#define WIFI_CONNECTED_BIT BIT0
|
#define WIFI_CONNECTED_BIT BIT0
|
||||||
#define WIFI_FAIL_BIT BIT1
|
#define WIFI_FAIL_BIT BIT1
|
||||||
|
#define WIFI_PROV_DONE_BIT BIT2
|
||||||
|
|
||||||
|
#define WIFI_STA_SSID_MAX_LEN 32
|
||||||
|
#define WIFI_STA_PASS_MAX_LEN 64
|
||||||
|
#define WIFI_PROV_HTTP_BODY_MAX_LEN 512
|
||||||
|
#define WIFI_SCAN_RESULT_LIMIT 20
|
||||||
|
|
||||||
|
#define WIFI_PROV_NVS_NAMESPACE "wifi_cfg"
|
||||||
|
#define WIFI_PROV_NVS_KEY_SSID "ssid"
|
||||||
|
#define WIFI_PROV_NVS_KEY_PASS "password"
|
||||||
|
|
||||||
|
#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_SSID
|
||||||
|
#define CONFIG_TQ_WIFI_PROV_SOFTAP_SSID "TalkingQ-Setup"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD
|
||||||
|
#define CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL
|
||||||
|
#define CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN
|
||||||
|
#define CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char ssid[WIFI_STA_SSID_MAX_LEN + 1];
|
||||||
|
char password[WIFI_STA_PASS_MAX_LEN + 1];
|
||||||
|
} wifi_sta_credentials_t;
|
||||||
|
|
||||||
static const char *TAG = "wifi_manager";
|
static const char *TAG = "wifi_manager";
|
||||||
|
extern const uint8_t s_provision_page_start[] asm("_binary_wifi_configuration_html_start");
|
||||||
|
extern const uint8_t s_provision_page_end[] asm("_binary_wifi_configuration_html_end");
|
||||||
|
|
||||||
static EventGroupHandle_t s_wifi_event_group;
|
static EventGroupHandle_t s_wifi_event_group;
|
||||||
static int s_retry_num;
|
static int s_retry_num;
|
||||||
static bool s_ready;
|
static bool s_ready;
|
||||||
static bool s_started;
|
static bool s_started;
|
||||||
|
static bool s_sta_reconnect_enabled;
|
||||||
|
static bool s_provisioning_active;
|
||||||
static esp_netif_t *s_sta_netif;
|
static esp_netif_t *s_sta_netif;
|
||||||
|
static esp_netif_t *s_ap_netif;
|
||||||
|
static httpd_handle_t s_prov_httpd;
|
||||||
static esp_event_handler_instance_t s_wifi_event_inst;
|
static esp_event_handler_instance_t s_wifi_event_inst;
|
||||||
static esp_event_handler_instance_t s_ip_event_inst;
|
static esp_event_handler_instance_t s_ip_event_inst;
|
||||||
|
|
||||||
@@ -48,31 +89,113 @@ static void wifi_manager_apply_sta_throughput_profile(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_save_credentials(const wifi_sta_credentials_t *creds) {
|
||||||
|
if (creds == NULL || creds->ssid[0] == '\0') {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_handle_t nvs = 0;
|
||||||
|
esp_err_t err = nvs_open(WIFI_PROV_NVS_NAMESPACE, NVS_READWRITE, &nvs);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_set_str(nvs, WIFI_PROV_NVS_KEY_SSID, creds->ssid);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = nvs_set_str(nvs, WIFI_PROV_NVS_KEY_PASS, creds->password);
|
||||||
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = nvs_commit(nvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_close(nvs);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_load_credentials(wifi_sta_credentials_t *out_creds) {
|
||||||
|
if (out_creds == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(out_creds, 0, sizeof(*out_creds));
|
||||||
|
|
||||||
|
nvs_handle_t nvs = 0;
|
||||||
|
esp_err_t err = nvs_open(WIFI_PROV_NVS_NAMESPACE, NVS_READONLY, &nvs);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ssid_len = sizeof(out_creds->ssid);
|
||||||
|
err = nvs_get_str(nvs, WIFI_PROV_NVS_KEY_SSID, out_creds->ssid, &ssid_len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
nvs_close(nvs);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pass_len = sizeof(out_creds->password);
|
||||||
|
err = nvs_get_str(nvs, WIFI_PROV_NVS_KEY_PASS, out_creds->password, &pass_len);
|
||||||
|
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||||
|
out_creds->password[0] = '\0';
|
||||||
|
err = ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_close(nvs);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
if (out_creds->ssid[0] == '\0') {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_manager_record_connect_result(esp_err_t err, const char *stage) {
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_SUCCESS, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err == ESP_ERR_TIMEOUT) {
|
||||||
|
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_TIMEOUT, 1);
|
||||||
|
} else {
|
||||||
|
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_FAILED, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime_diag_record_error("wifi_connect", err, stage);
|
||||||
|
}
|
||||||
|
|
||||||
static void wifi_event_handler(void *arg,
|
static void wifi_event_handler(void *arg,
|
||||||
esp_event_base_t event_base,
|
esp_event_base_t event_base,
|
||||||
int32_t event_id,
|
int32_t event_id,
|
||||||
void *event_data) {
|
void *event_data) {
|
||||||
(void)arg;
|
(void)arg;
|
||||||
(void)event_data;
|
|
||||||
|
|
||||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||||
if (!s_started) {
|
// Connection is triggered explicitly by wifi_manager_connect_sta().
|
||||||
return;
|
// Keep this event hook only for lifecycle visibility.
|
||||||
}
|
|
||||||
(void)esp_wifi_connect();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
s_ready = false;
|
s_ready = false;
|
||||||
if (!s_started) {
|
if (!s_started || !s_sta_reconnect_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data;
|
||||||
|
uint8_t reason = disc != NULL ? disc->reason : 0;
|
||||||
|
|
||||||
if (s_retry_num < CONFIG_TQ_WIFI_MAXIMUM_RETRY) {
|
if (s_retry_num < CONFIG_TQ_WIFI_MAXIMUM_RETRY) {
|
||||||
(void)esp_wifi_connect();
|
(void)esp_wifi_connect();
|
||||||
s_retry_num++;
|
s_retry_num++;
|
||||||
ESP_LOGW(TAG, "retry to connect to the AP (%d/%d)", s_retry_num, CONFIG_TQ_WIFI_MAXIMUM_RETRY);
|
ESP_LOGW(TAG,
|
||||||
|
"retry to connect AP (%d/%d), reason=%u",
|
||||||
|
s_retry_num,
|
||||||
|
CONFIG_TQ_WIFI_MAXIMUM_RETRY,
|
||||||
|
(unsigned)reason);
|
||||||
} else if (s_wifi_event_group != NULL) {
|
} else if (s_wifi_event_group != NULL) {
|
||||||
|
ESP_LOGW(TAG, "connect failed after retries, reason=%u", (unsigned)reason);
|
||||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -82,6 +205,7 @@ static void wifi_event_handler(void *arg,
|
|||||||
if (!s_started) {
|
if (!s_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||||
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||||
s_retry_num = 0;
|
s_retry_num = 0;
|
||||||
@@ -89,37 +213,309 @@ static void wifi_event_handler(void *arg,
|
|||||||
if (s_wifi_event_group != NULL) {
|
if (s_wifi_event_group != NULL) {
|
||||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t start_sta_mode(void) {
|
static int wifi_manager_hex_digit_to_int(char c) {
|
||||||
wifi_config_t wifi_config = {0};
|
if (c >= '0' && c <= '9') {
|
||||||
if (strlen(CONFIG_TQ_WIFI_SSID) == 0) {
|
return c - '0';
|
||||||
ESP_LOGE(TAG, "CONFIG_TQ_WIFI_SSID is empty; STA-only mode requires valid SSID");
|
}
|
||||||
return ESP_ERR_INVALID_STATE;
|
c = (char)tolower((unsigned char)c);
|
||||||
|
if (c >= 'a' && c <= 'f') {
|
||||||
|
return 10 + (c - 'a');
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_manager_url_decode(char *dst, size_t dst_len, const char *src) {
|
||||||
|
if (dst == NULL || dst_len == 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
strlcpy((char *)wifi_config.sta.ssid, CONFIG_TQ_WIFI_SSID, sizeof(wifi_config.sta.ssid));
|
size_t wi = 0;
|
||||||
strlcpy((char *)wifi_config.sta.password, CONFIG_TQ_WIFI_PASSWORD, sizeof(wifi_config.sta.password));
|
for (size_t ri = 0; src != NULL && src[ri] != '\0' && wi + 1 < dst_len; ++ri) {
|
||||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
if (src[ri] == '+') {
|
||||||
|
dst[wi++] = ' ';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src[ri] == '%' && src[ri + 1] != '\0' && src[ri + 2] != '\0') {
|
||||||
|
int hi = wifi_manager_hex_digit_to_int(src[ri + 1]);
|
||||||
|
int lo = wifi_manager_hex_digit_to_int(src[ri + 2]);
|
||||||
|
if (hi >= 0 && lo >= 0) {
|
||||||
|
dst[wi++] = (char)((hi << 4) | lo);
|
||||||
|
ri += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[wi++] = src[ri];
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[wi] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wifi_manager_form_get_value(const char *form,
|
||||||
|
const char *key,
|
||||||
|
char *out,
|
||||||
|
size_t out_len) {
|
||||||
|
if (form == NULL || key == NULL || out == NULL || out_len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t key_len = strlen(key);
|
||||||
|
const char *cursor = form;
|
||||||
|
|
||||||
|
while (*cursor != '\0') {
|
||||||
|
const char *segment_end = strchr(cursor, '&');
|
||||||
|
size_t segment_len = segment_end != NULL ? (size_t)(segment_end - cursor) : strlen(cursor);
|
||||||
|
const char *eq = memchr(cursor, '=', segment_len);
|
||||||
|
|
||||||
|
if (eq != NULL && (size_t)(eq - cursor) == key_len && strncmp(cursor, key, key_len) == 0) {
|
||||||
|
size_t encoded_len = segment_len - key_len - 1;
|
||||||
|
char encoded[WIFI_PROV_HTTP_BODY_MAX_LEN];
|
||||||
|
if (encoded_len >= sizeof(encoded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(encoded, eq + 1, encoded_len);
|
||||||
|
encoded[encoded_len] = '\0';
|
||||||
|
wifi_manager_url_decode(out, out_len, encoded);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment_end == NULL) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = segment_end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[0] = '\0';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wifi_manager_is_url_unreserved(char c) {
|
||||||
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
|
||||||
|
c == '-' || c == '_' || c == '.' || c == '~';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_manager_url_encode(const char *src, char *dst, size_t dst_len) {
|
||||||
|
if (dst == NULL || dst_len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t wi = 0;
|
||||||
|
for (size_t ri = 0; src != NULL && src[ri] != '\0' && wi + 1 < dst_len; ++ri) {
|
||||||
|
unsigned char ch = (unsigned char)src[ri];
|
||||||
|
if (wifi_manager_is_url_unreserved((char)ch)) {
|
||||||
|
dst[wi++] = (char)ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wi + 3 >= dst_len) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char hex[] = "0123456789ABCDEF";
|
||||||
|
dst[wi++] = '%';
|
||||||
|
dst[wi++] = hex[(ch >> 4) & 0x0F];
|
||||||
|
dst[wi++] = hex[ch & 0x0F];
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[wi] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *wifi_manager_authmode_to_str(wifi_auth_mode_t authmode) {
|
||||||
|
switch (authmode) {
|
||||||
|
case WIFI_AUTH_OPEN:
|
||||||
|
return "open";
|
||||||
|
case WIFI_AUTH_WEP:
|
||||||
|
return "wep";
|
||||||
|
case WIFI_AUTH_WPA_PSK:
|
||||||
|
return "wpa";
|
||||||
|
case WIFI_AUTH_WPA2_PSK:
|
||||||
|
return "wpa2";
|
||||||
|
case WIFI_AUTH_WPA_WPA2_PSK:
|
||||||
|
return "wpa_wpa2";
|
||||||
|
case WIFI_AUTH_WPA2_ENTERPRISE:
|
||||||
|
return "wpa2_enterprise";
|
||||||
|
case WIFI_AUTH_WPA3_PSK:
|
||||||
|
return "wpa3";
|
||||||
|
case WIFI_AUTH_WPA2_WPA3_PSK:
|
||||||
|
return "wpa2_wpa3";
|
||||||
|
case WIFI_AUTH_WAPI_PSK:
|
||||||
|
return "wapi";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_send_prov_json(httpd_req_t *req, bool ok, const char *error_msg) {
|
||||||
|
char payload[192] = {0};
|
||||||
|
if (ok) {
|
||||||
|
strlcpy(payload, "{\"ok\":true}", sizeof(payload));
|
||||||
|
} else {
|
||||||
|
snprintf(payload,
|
||||||
|
sizeof(payload),
|
||||||
|
"{\"ok\":false,\"error\":\"%s\"}",
|
||||||
|
error_msg != NULL ? error_msg : "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||||
|
return httpd_resp_sendstr(req, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_scan_get_handler(httpd_req_t *req) {
|
||||||
|
wifi_scan_config_t scan_cfg = {
|
||||||
|
.show_hidden = false,
|
||||||
|
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_start failed");
|
||||||
|
return wifi_manager_send_prov_json(req, false, "scan_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ap_count = 0;
|
||||||
|
err = esp_wifi_scan_get_ap_num(&ap_count);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_get_ap_num failed");
|
||||||
|
return wifi_manager_send_prov_json(req, false, "scan_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ap_count > WIFI_SCAN_RESULT_LIMIT) {
|
||||||
|
ap_count = WIFI_SCAN_RESULT_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_ap_record_t *records = NULL;
|
||||||
|
if (ap_count > 0) {
|
||||||
|
records = calloc(ap_count, sizeof(*records));
|
||||||
|
if (records == NULL) {
|
||||||
|
return wifi_manager_send_prov_json(req, false, "oom");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t fetch_num = ap_count;
|
||||||
|
err = esp_wifi_scan_get_ap_records(&fetch_num, records);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
free(records);
|
||||||
|
runtime_diag_record_error("wifi_scan", err, "esp_wifi_scan_get_ap_records failed");
|
||||||
|
return wifi_manager_send_prov_json(req, false, "scan_failed");
|
||||||
|
}
|
||||||
|
ap_count = fetch_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||||
|
|
||||||
|
err = httpd_resp_sendstr_chunk(req, "{\"ok\":true,\"networks\":[");
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
free(records);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
char seen[WIFI_SCAN_RESULT_LIMIT][WIFI_STA_SSID_MAX_LEN + 1] = {{0}};
|
||||||
|
size_t seen_count = 0;
|
||||||
|
|
||||||
|
for (uint16_t i = 0; i < ap_count; ++i) {
|
||||||
|
char ssid_buf[WIFI_STA_SSID_MAX_LEN + 1] = {0};
|
||||||
|
memcpy(ssid_buf, records[i].ssid, WIFI_STA_SSID_MAX_LEN);
|
||||||
|
if (ssid_buf[0] == '\0') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool duplicate = false;
|
||||||
|
for (size_t j = 0; j < seen_count; ++j) {
|
||||||
|
if (strncmp(seen[j], ssid_buf, WIFI_STA_SSID_MAX_LEN) == 0) {
|
||||||
|
duplicate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (duplicate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen_count < WIFI_SCAN_RESULT_LIMIT) {
|
||||||
|
strlcpy(seen[seen_count], ssid_buf, sizeof(seen[seen_count]));
|
||||||
|
seen_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
char ssid_encoded[(WIFI_STA_SSID_MAX_LEN * 3) + 1];
|
||||||
|
wifi_manager_url_encode(ssid_buf, ssid_encoded, sizeof(ssid_encoded));
|
||||||
|
|
||||||
|
char item[196];
|
||||||
|
snprintf(item,
|
||||||
|
sizeof(item),
|
||||||
|
"%s{\"ssid\":\"%s\",\"rssi\":%d,\"auth\":\"%s\"}",
|
||||||
|
first ? "" : ",",
|
||||||
|
ssid_encoded,
|
||||||
|
records[i].rssi,
|
||||||
|
wifi_manager_authmode_to_str(records[i].authmode));
|
||||||
|
|
||||||
|
err = httpd_resp_sendstr_chunk(req, item);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
free(records);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(records);
|
||||||
|
err = httpd_resp_sendstr_chunk(req, "]}");
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = httpd_resp_sendstr_chunk(req, NULL);
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_connect_sta(const wifi_sta_credentials_t *creds, bool keep_softap) {
|
||||||
|
if (creds == NULL || creds->ssid[0] == '\0') {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_config_t wifi_config = {0};
|
||||||
|
strlcpy((char *)wifi_config.sta.ssid, creds->ssid, sizeof(wifi_config.sta.ssid));
|
||||||
|
strlcpy((char *)wifi_config.sta.password, creds->password, sizeof(wifi_config.sta.password));
|
||||||
|
wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||||
wifi_config.sta.pmf_cfg.capable = true;
|
wifi_config.sta.pmf_cfg.capable = true;
|
||||||
wifi_config.sta.pmf_cfg.required = false;
|
wifi_config.sta.pmf_cfg.required = false;
|
||||||
|
|
||||||
esp_err_t rc = esp_wifi_set_mode(WIFI_MODE_STA);
|
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
|
||||||
if (rc != ESP_OK) {
|
s_retry_num = 0;
|
||||||
return rc;
|
s_ready = false;
|
||||||
|
|
||||||
|
s_sta_reconnect_enabled = false;
|
||||||
|
esp_err_t err = esp_wifi_disconnect();
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED && err != ESP_ERR_WIFI_CONN) {
|
||||||
|
ESP_LOGW(TAG, "esp_wifi_disconnect before connect failed, err=0x%x", (unsigned)err);
|
||||||
}
|
}
|
||||||
rc = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
|
s_sta_reconnect_enabled = true;
|
||||||
if (rc != ESP_OK) {
|
|
||||||
return rc;
|
err = esp_wifi_set_mode(keep_softap ? WIFI_MODE_APSTA : WIFI_MODE_STA);
|
||||||
}
|
if (err != ESP_OK) {
|
||||||
rc = esp_wifi_start();
|
wifi_manager_record_connect_result(err, "set mode failed");
|
||||||
if (rc != ESP_OK && rc != ESP_ERR_INVALID_STATE) {
|
return err;
|
||||||
return rc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "wifi_init_sta finished");
|
err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
wifi_manager_record_connect_result(err, "set sta config failed");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_wifi_start();
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
|
wifi_manager_record_connect_result(err, "wifi start failed");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_wifi_connect();
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) {
|
||||||
|
wifi_manager_record_connect_result(err, "wifi connect failed");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "trying to connect to SSID:%s", creds->ssid);
|
||||||
|
|
||||||
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
||||||
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
||||||
@@ -128,7 +524,7 @@ static esp_err_t start_sta_mode(void) {
|
|||||||
pdMS_TO_TICKS(runtime_policy_wifi_connect_timeout_ms()));
|
pdMS_TO_TICKS(runtime_policy_wifi_connect_timeout_ms()));
|
||||||
|
|
||||||
if (bits & WIFI_CONNECTED_BIT) {
|
if (bits & WIFI_CONNECTED_BIT) {
|
||||||
ESP_LOGI(TAG, "connected to AP SSID:%s", CONFIG_TQ_WIFI_SSID);
|
ESP_LOGI(TAG, "connected to AP SSID:%s", creds->ssid);
|
||||||
wifi_manager_apply_sta_throughput_profile();
|
wifi_manager_apply_sta_throughput_profile();
|
||||||
esp_err_t ps_rc = esp_wifi_set_ps(WIFI_PS_NONE);
|
esp_err_t ps_rc = esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
if (ps_rc == ESP_OK) {
|
if (ps_rc == ESP_OK) {
|
||||||
@@ -137,18 +533,243 @@ static esp_err_t start_sta_mode(void) {
|
|||||||
ESP_LOGW(TAG, "failed to set WIFI_PS_NONE, rc=0x%x", (unsigned)ps_rc);
|
ESP_LOGW(TAG, "failed to set WIFI_PS_NONE, rc=0x%x", (unsigned)ps_rc);
|
||||||
}
|
}
|
||||||
s_ready = true;
|
s_ready = true;
|
||||||
|
wifi_manager_record_connect_result(ESP_OK, "connect success");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bits & WIFI_FAIL_BIT) {
|
if (bits & WIFI_FAIL_BIT) {
|
||||||
ESP_LOGW(TAG, "Failed to connect to SSID:%s", CONFIG_TQ_WIFI_SSID);
|
ESP_LOGW(TAG, "failed to connect to SSID:%s", creds->ssid);
|
||||||
|
wifi_manager_record_connect_result(ESP_FAIL, "connect failed after retries");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGW(TAG, "Wi-Fi connect timeout");
|
ESP_LOGW(TAG, "Wi-Fi connect timeout, SSID:%s", creds->ssid);
|
||||||
|
wifi_manager_record_connect_result(ESP_ERR_TIMEOUT, "connect timeout");
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_prov_index_get_handler(httpd_req_t *req) {
|
||||||
|
httpd_resp_set_type(req, "text/html");
|
||||||
|
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
|
||||||
|
size_t page_len = (size_t)(s_provision_page_end - s_provision_page_start);
|
||||||
|
return httpd_resp_send(req, (const char *)s_provision_page_start, page_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_prov_submit_post_handler(httpd_req_t *req) {
|
||||||
|
if (req->content_len <= 0 || req->content_len >= WIFI_PROV_HTTP_BODY_MAX_LEN) {
|
||||||
|
return wifi_manager_send_prov_json(req, false, "invalid_payload");
|
||||||
|
}
|
||||||
|
|
||||||
|
char body[WIFI_PROV_HTTP_BODY_MAX_LEN] = {0};
|
||||||
|
int received_total = 0;
|
||||||
|
|
||||||
|
while (received_total < req->content_len) {
|
||||||
|
int ret = httpd_req_recv(req, body + received_total, req->content_len - received_total);
|
||||||
|
if (ret > 0) {
|
||||||
|
received_total += ret;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return wifi_manager_send_prov_json(req, false, "read_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
body[received_total] = '\0';
|
||||||
|
|
||||||
|
wifi_sta_credentials_t creds = {0};
|
||||||
|
bool has_ssid = wifi_manager_form_get_value(body, "ssid", creds.ssid, sizeof(creds.ssid));
|
||||||
|
(void)wifi_manager_form_get_value(body, "password", creds.password, sizeof(creds.password));
|
||||||
|
|
||||||
|
if (!has_ssid || creds.ssid[0] == '\0') {
|
||||||
|
return wifi_manager_send_prov_json(req, false, "ssid_required");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t password_len = strlen(creds.password);
|
||||||
|
if (password_len > 0 && password_len < 8) {
|
||||||
|
return wifi_manager_send_prov_json(req, false, "password_too_short");
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = wifi_manager_connect_sta(&creds, true);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return wifi_manager_send_prov_json(req, false, "connect_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wifi_manager_save_credentials(&creds);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
runtime_diag_record_error("wifi_provision", err, "save credentials failed");
|
||||||
|
s_ready = false;
|
||||||
|
(void)esp_wifi_disconnect();
|
||||||
|
return wifi_manager_send_prov_json(req, false, "save_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Wi-Fi provisioning succeeded, credentials stored in NVS");
|
||||||
|
if (s_wifi_event_group != NULL) {
|
||||||
|
xEventGroupSetBits(s_wifi_event_group, WIFI_PROV_DONE_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wifi_manager_send_prov_json(req, true, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_start_prov_http_server(void) {
|
||||||
|
if (s_prov_httpd != NULL) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
config.server_port = 80;
|
||||||
|
config.max_uri_handlers = 8;
|
||||||
|
|
||||||
|
esp_err_t err = httpd_start(&s_prov_httpd, &config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpd_uri_t index_uri = {
|
||||||
|
.uri = "/",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = wifi_manager_prov_index_get_handler,
|
||||||
|
.user_ctx = NULL,
|
||||||
|
};
|
||||||
|
err = httpd_register_uri_handler(s_prov_httpd, &index_uri);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
httpd_stop(s_prov_httpd);
|
||||||
|
s_prov_httpd = NULL;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpd_uri_t scan_uri = {
|
||||||
|
.uri = "/scan",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = wifi_manager_scan_get_handler,
|
||||||
|
.user_ctx = NULL,
|
||||||
|
};
|
||||||
|
err = httpd_register_uri_handler(s_prov_httpd, &scan_uri);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
httpd_stop(s_prov_httpd);
|
||||||
|
s_prov_httpd = NULL;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpd_uri_t submit_uri = {
|
||||||
|
.uri = "/provision",
|
||||||
|
.method = HTTP_POST,
|
||||||
|
.handler = wifi_manager_prov_submit_post_handler,
|
||||||
|
.user_ctx = NULL,
|
||||||
|
};
|
||||||
|
err = httpd_register_uri_handler(s_prov_httpd, &submit_uri);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
httpd_stop(s_prov_httpd);
|
||||||
|
s_prov_httpd = NULL;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_manager_stop_prov_http_server(void) {
|
||||||
|
if (s_prov_httpd == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_stop(s_prov_httpd);
|
||||||
|
s_prov_httpd = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_start_softap_provisioning(void) {
|
||||||
|
wifi_config_t ap_config = {0};
|
||||||
|
|
||||||
|
const char *ap_ssid = CONFIG_TQ_WIFI_PROV_SOFTAP_SSID;
|
||||||
|
if (ap_ssid == NULL || ap_ssid[0] == '\0') {
|
||||||
|
ap_ssid = "TalkingQ-Setup";
|
||||||
|
}
|
||||||
|
|
||||||
|
strlcpy((char *)ap_config.ap.ssid, ap_ssid, sizeof(ap_config.ap.ssid));
|
||||||
|
ap_config.ap.ssid_len = strlen((const char *)ap_config.ap.ssid);
|
||||||
|
ap_config.ap.channel = CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL;
|
||||||
|
ap_config.ap.max_connection = CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN;
|
||||||
|
ap_config.ap.pmf_cfg.capable = true;
|
||||||
|
ap_config.ap.pmf_cfg.required = false;
|
||||||
|
|
||||||
|
const char *ap_password = CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD;
|
||||||
|
if (ap_password != NULL && strlen(ap_password) >= 8) {
|
||||||
|
strlcpy((char *)ap_config.ap.password, ap_password, sizeof(ap_config.ap.password));
|
||||||
|
ap_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||||
|
} else {
|
||||||
|
ap_config.ap.password[0] = '\0';
|
||||||
|
ap_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
xEventGroupClearBits(s_wifi_event_group, WIFI_PROV_DONE_BIT | WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
|
||||||
|
|
||||||
|
esp_err_t err = esp_wifi_set_mode(WIFI_MODE_APSTA);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_wifi_set_config(WIFI_IF_AP, &ap_config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_wifi_start();
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wifi_manager_start_prov_http_server();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_provisioning_active = true;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"SoftAP provisioning started: ssid=%s password=%s ip=192.168.4.1",
|
||||||
|
ap_ssid,
|
||||||
|
(ap_password != NULL && strlen(ap_password) >= 8) ? "***" : "<open>");
|
||||||
|
ESP_LOGI(TAG, "open http://192.168.4.1 from a phone/computer connected to this AP");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wifi_manager_stop_softap_provisioning(void) {
|
||||||
|
if (!s_provisioning_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_manager_stop_prov_http_server();
|
||||||
|
|
||||||
|
esp_err_t err = esp_wifi_set_mode(WIFI_MODE_STA);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "failed to switch to STA mode after provisioning, err=0x%x", (unsigned)err);
|
||||||
|
}
|
||||||
|
|
||||||
|
s_provisioning_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t wifi_manager_run_softap_provisioning(void) {
|
||||||
|
esp_err_t err = wifi_manager_start_softap_provisioning();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (s_started) {
|
||||||
|
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
||||||
|
WIFI_PROV_DONE_BIT,
|
||||||
|
pdTRUE,
|
||||||
|
pdFALSE,
|
||||||
|
pdMS_TO_TICKS(500));
|
||||||
|
if (bits & WIFI_PROV_DONE_BIT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wifi_manager_stop_softap_provisioning();
|
||||||
|
|
||||||
|
return s_ready ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t wifi_manager_start(void) {
|
esp_err_t wifi_manager_start(void) {
|
||||||
if (s_started) {
|
if (s_started) {
|
||||||
return s_ready ? ESP_OK : ESP_ERR_INVALID_STATE;
|
return s_ready ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||||
@@ -156,6 +777,8 @@ esp_err_t wifi_manager_start(void) {
|
|||||||
|
|
||||||
s_retry_num = 0;
|
s_retry_num = 0;
|
||||||
s_ready = false;
|
s_ready = false;
|
||||||
|
s_sta_reconnect_enabled = true;
|
||||||
|
s_provisioning_active = false;
|
||||||
|
|
||||||
if (s_wifi_event_group == NULL) {
|
if (s_wifi_event_group == NULL) {
|
||||||
s_wifi_event_group = xEventGroupCreate();
|
s_wifi_event_group = xEventGroupCreate();
|
||||||
@@ -163,7 +786,7 @@ esp_err_t wifi_manager_start(void) {
|
|||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
|
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT | WIFI_PROV_DONE_BIT);
|
||||||
|
|
||||||
esp_err_t err = esp_netif_init();
|
esp_err_t err = esp_netif_init();
|
||||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
@@ -185,6 +808,14 @@ esp_err_t wifi_manager_start(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_ap_netif == NULL) {
|
||||||
|
s_ap_netif = esp_netif_create_default_wifi_ap();
|
||||||
|
if (s_ap_netif == NULL) {
|
||||||
|
runtime_diag_record_error("wifi_start", ESP_ERR_NO_MEM, "create ap netif failed");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
err = esp_wifi_init(&cfg);
|
err = esp_wifi_init(&cfg);
|
||||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
@@ -192,6 +823,12 @@ esp_err_t wifi_manager_start(void) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = esp_wifi_set_storage(WIFI_STORAGE_RAM);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
runtime_diag_record_error("wifi_start", err, "esp_wifi_set_storage RAM failed");
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_wifi_event_inst == NULL) {
|
if (s_wifi_event_inst == NULL) {
|
||||||
err = esp_event_handler_instance_register(WIFI_EVENT,
|
err = esp_event_handler_instance_register(WIFI_EVENT,
|
||||||
ESP_EVENT_ANY_ID,
|
ESP_EVENT_ANY_ID,
|
||||||
@@ -217,18 +854,26 @@ esp_err_t wifi_manager_start(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_started = true;
|
s_started = true;
|
||||||
err = start_sta_mode();
|
|
||||||
|
wifi_sta_credentials_t saved = {0};
|
||||||
|
err = wifi_manager_load_credentials(&saved);
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_SUCCESS, 1);
|
ESP_LOGI(TAG, "found saved Wi-Fi credentials for SSID:%s", saved.ssid);
|
||||||
} else {
|
err = wifi_manager_connect_sta(&saved, false);
|
||||||
if (err == ESP_ERR_TIMEOUT) {
|
if (err == ESP_OK) {
|
||||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_TIMEOUT, 1);
|
return ESP_OK;
|
||||||
} else {
|
|
||||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_WIFI_CONNECT_FAILED, 1);
|
|
||||||
}
|
}
|
||||||
runtime_diag_record_error("wifi_connect", err, "STA connect failed");
|
ESP_LOGW(TAG, "saved Wi-Fi credentials failed, starting SoftAP provisioning");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "no valid saved Wi-Fi credentials, starting SoftAP provisioning");
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wifi_manager_run_softap_provisioning();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
runtime_diag_record_error("wifi_start", err, "SoftAP provisioning failed");
|
||||||
(void)wifi_manager_stop();
|
(void)wifi_manager_stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +886,9 @@ esp_err_t wifi_manager_stop(void) {
|
|||||||
|
|
||||||
s_ready = false;
|
s_ready = false;
|
||||||
s_retry_num = 0;
|
s_retry_num = 0;
|
||||||
|
s_sta_reconnect_enabled = false;
|
||||||
|
|
||||||
|
wifi_manager_stop_softap_provisioning();
|
||||||
|
|
||||||
if (s_wifi_event_inst != NULL) {
|
if (s_wifi_event_inst != NULL) {
|
||||||
esp_err_t err = esp_event_handler_instance_unregister(WIFI_EVENT,
|
esp_err_t err = esp_event_handler_instance_unregister(WIFI_EVENT,
|
||||||
@@ -288,6 +936,11 @@ esp_err_t wifi_manager_stop(void) {
|
|||||||
s_sta_netif = NULL;
|
s_sta_netif = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_ap_netif != NULL) {
|
||||||
|
esp_netif_destroy_default_wifi(s_ap_netif);
|
||||||
|
s_ap_netif = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_wifi_event_group != NULL) {
|
if (s_wifi_event_group != NULL) {
|
||||||
vEventGroupDelete(s_wifi_event_group);
|
vEventGroupDelete(s_wifi_event_group);
|
||||||
s_wifi_event_group = NULL;
|
s_wifi_event_group = NULL;
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
menu "TQ Controller Config"
|
menu "TQ Controller Config"
|
||||||
|
|
||||||
config TQ_WIFI_SSID
|
config TQ_WIFI_PROV_SOFTAP_SSID
|
||||||
string "Wi-Fi SSID"
|
string "Wi-Fi provisioning SoftAP SSID"
|
||||||
|
default "TalkingQ-Setup"
|
||||||
|
|
||||||
|
config TQ_WIFI_PROV_SOFTAP_PASSWORD
|
||||||
|
string "Wi-Fi provisioning SoftAP Password (empty means open AP)"
|
||||||
default ""
|
default ""
|
||||||
|
|
||||||
config TQ_WIFI_PASSWORD
|
config TQ_WIFI_PROV_SOFTAP_CHANNEL
|
||||||
string "Wi-Fi Password"
|
int "Wi-Fi provisioning SoftAP channel"
|
||||||
default ""
|
range 1 13
|
||||||
|
default 1
|
||||||
|
|
||||||
|
config TQ_WIFI_PROV_SOFTAP_MAX_CONN
|
||||||
|
int "Wi-Fi provisioning SoftAP max clients"
|
||||||
|
range 1 8
|
||||||
|
default 4
|
||||||
|
|
||||||
config TQ_WIFI_MAXIMUM_RETRY
|
config TQ_WIFI_MAXIMUM_RETRY
|
||||||
int "Wi-Fi Maximum Retry"
|
int "Wi-Fi Maximum Retry"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ CONFIG_BT_NIMBLE_ENABLED=y
|
|||||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||||
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
|
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
|
||||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247
|
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247
|
||||||
# CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set
|
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y
|
||||||
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
|
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
|
||||||
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=24
|
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=24
|
||||||
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=256
|
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=256
|
||||||
@@ -18,8 +18,10 @@ CONFIG_LWIP_TCP_SND_BUF_DEFAULT=57344
|
|||||||
CONFIG_LWIP_TCP_WND_DEFAULT=262144
|
CONFIG_LWIP_TCP_WND_DEFAULT=262144
|
||||||
CONFIG_LWIP_TCP_RECVMBOX_SIZE=192
|
CONFIG_LWIP_TCP_RECVMBOX_SIZE=192
|
||||||
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=256
|
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=256
|
||||||
CONFIG_TQ_WIFI_SSID="TalkingQ"
|
CONFIG_TQ_WIFI_PROV_SOFTAP_SSID="TalkingQ-Setup"
|
||||||
CONFIG_TQ_WIFI_PASSWORD="TalkingQ123"
|
CONFIG_TQ_WIFI_PROV_SOFTAP_PASSWORD=""
|
||||||
|
CONFIG_TQ_WIFI_PROV_SOFTAP_CHANNEL=1
|
||||||
|
CONFIG_TQ_WIFI_PROV_SOFTAP_MAX_CONN=4
|
||||||
CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE="512*768"
|
CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE="512*768"
|
||||||
CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES=16384
|
CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES=16384
|
||||||
CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES=16384
|
CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES=16384
|
||||||
|
|||||||
Reference in New Issue
Block a user