forked from xiaozhi/xiaozhi-esp32
添加了一个声波配网的HTML界面 (#996)
* Add HTML page for sonic-based Wi-Fi provisioning * Add HTML page for sonic-based Wi-Fi provisioning * Add soundwave provisioning UI and fix bugs * Add soundwave provisioning UI and fix bugs * Move sonic_wifi_config.html to the docs folder --------- Co-authored-by: luyuhan <xiaolumylove@icloud.com>
This commit is contained in:
208
docs/sonic_wifi_config.html
Normal file
208
docs/sonic_wifi_config.html
Normal file
@@ -0,0 +1,208 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>小智声波配网</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", "PingFang SC", sans-serif;
|
||||
background: #f0f2f5;
|
||||
margin: 0;
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
padding: 2rem 1.5rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin: 1rem 0 0.3rem;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.checkbox-container {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
padding: 0.8rem;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #4a90e2;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #357ab8;
|
||||
}
|
||||
button:active {
|
||||
background-color: #2f6ea2;
|
||||
}
|
||||
audio {
|
||||
margin-top: 1.5rem;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>📶 小智声波配网</h2>
|
||||
<label for="ssid">WiFi 名称</label>
|
||||
<input id="ssid" type="text" value="" placeholder="请输入 WiFi 名称" />
|
||||
|
||||
<label for="pwd">WiFi 密码</label>
|
||||
<input id="pwd" type="password" value="" placeholder="请输入 WiFi 密码" />
|
||||
|
||||
<div class="checkbox-container">
|
||||
<label><input type="checkbox" id="loopCheck" checked /> 自动循环播放声波</label>
|
||||
</div>
|
||||
|
||||
<button onclick="generate()">🎵 生成并播放声波</button>
|
||||
<button onclick="stopPlay()">⏹️ 停止播放</button>
|
||||
<audio id="player" controls></audio>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const MARK = 1800;
|
||||
const SPACE = 1500;
|
||||
const SAMPLE_RATE = 44100;
|
||||
const BIT_RATE = 100;
|
||||
const START_BYTES = [0x01, 0x02];
|
||||
const END_BYTES = [0x03, 0x04];
|
||||
let loopTimer = null;
|
||||
|
||||
function checksum(data) {
|
||||
return data.reduce((sum, b) => (sum + b) & 0xff, 0);
|
||||
}
|
||||
|
||||
function toBits(byte) {
|
||||
const bits = [];
|
||||
for (let i = 7; i >= 0; i--) bits.push((byte >> i) & 1);
|
||||
return bits;
|
||||
}
|
||||
|
||||
function afskModulate(bits) {
|
||||
const samplesPerBit = SAMPLE_RATE / BIT_RATE;
|
||||
const totalSamples = Math.floor(bits.length * samplesPerBit);
|
||||
const buffer = new Float32Array(totalSamples);
|
||||
for (let i = 0; i < bits.length; i++) {
|
||||
const freq = bits[i] ? MARK : SPACE;
|
||||
for (let j = 0; j < samplesPerBit; j++) {
|
||||
const t = (i * samplesPerBit + j) / SAMPLE_RATE;
|
||||
buffer[i * samplesPerBit + j] = Math.sin(2 * Math.PI * freq * t);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(floatSamples) {
|
||||
const buffer = new Uint8Array(floatSamples.length * 2);
|
||||
for (let i = 0; i < floatSamples.length; i++) {
|
||||
const s = Math.max(-1, Math.min(1, floatSamples[i]));
|
||||
const val = s < 0 ? s * 0x8000 : s * 0x7fff;
|
||||
buffer[i * 2] = val & 0xff;
|
||||
buffer[i * 2 + 1] = (val >> 8) & 0xff;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function buildWav(pcm) {
|
||||
const wavHeader = new Uint8Array(44);
|
||||
const dataLen = pcm.length;
|
||||
const fileLen = 36 + dataLen;
|
||||
|
||||
const writeStr = (offset, str) => {
|
||||
for (let i = 0; i < str.length; i++) wavHeader[offset + i] = str.charCodeAt(i);
|
||||
};
|
||||
const write32 = (offset, value) => {
|
||||
wavHeader[offset] = value & 0xff;
|
||||
wavHeader[offset + 1] = (value >> 8) & 0xff;
|
||||
wavHeader[offset + 2] = (value >> 16) & 0xff;
|
||||
wavHeader[offset + 3] = (value >> 24) & 0xff;
|
||||
};
|
||||
const write16 = (offset, value) => {
|
||||
wavHeader[offset] = value & 0xff;
|
||||
wavHeader[offset + 1] = (value >> 8) & 0xff;
|
||||
};
|
||||
|
||||
writeStr(0, 'RIFF');
|
||||
write32(4, fileLen);
|
||||
writeStr(8, 'WAVE');
|
||||
writeStr(12, 'fmt ');
|
||||
write32(16, 16);
|
||||
write16(20, 1);
|
||||
write16(22, 1);
|
||||
write32(24, SAMPLE_RATE);
|
||||
write32(28, SAMPLE_RATE * 2);
|
||||
write16(32, 2);
|
||||
write16(34, 16);
|
||||
writeStr(36, 'data');
|
||||
write32(40, dataLen);
|
||||
|
||||
return new Blob([wavHeader, pcm], { type: 'audio/wav' });
|
||||
}
|
||||
|
||||
function generate() {
|
||||
stopPlay();
|
||||
const ssid = document.getElementById('ssid').value.trim();
|
||||
const pwd = document.getElementById('pwd').value.trim();
|
||||
const dataStr = ssid + '\n' + pwd;
|
||||
const textBytes = Array.from(new TextEncoder().encode(dataStr));
|
||||
const fullBytes = [...START_BYTES, ...textBytes, checksum(textBytes), ...END_BYTES];
|
||||
|
||||
let bits = [];
|
||||
fullBytes.forEach((b) => (bits = bits.concat(toBits(b))));
|
||||
|
||||
const floatBuf = afskModulate(bits);
|
||||
const pcmBuf = floatTo16BitPCM(floatBuf);
|
||||
const wavBlob = buildWav(pcmBuf);
|
||||
|
||||
const audio = document.getElementById('player');
|
||||
audio.src = URL.createObjectURL(wavBlob);
|
||||
audio.load();
|
||||
audio.play();
|
||||
|
||||
// 修改了这里:使用 'ended' 事件来实现循环播放
|
||||
if (document.getElementById('loopCheck').checked) {
|
||||
audio.onended = function() {
|
||||
audio.currentTime = 0; // 从头开始
|
||||
audio.play(); // 重新播放
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function stopPlay() {
|
||||
const audio = document.getElementById('player');
|
||||
audio.pause();
|
||||
audio.onended = null; // 清除事件监听
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user