一 、使用说明
- 硬件
- 电压输入:GPIO34
- 分压电阻默认:10k + 4.7k (可在
read_voltage中修改) - PWM 输出:GPIO14
- LED:板载GPIO2
- 网页功能
- LED 开关
- PWM 滑块控制(500~1000)
- 实时电压显示(每秒自动刷新)
- API 接口
/light/on/light/off/pwm/xxx/voltage(返回:如3.72)
直接烧录、打开网页即可使用
二、实物怎么接?(一步一步)
【待测电压 VIN】
│
▼
R1 10k
│
├──────────→ GPIO34 (ADC)
│
R2 4.7k
│
▼
GND
二、修改后 b.html(完整代码)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>ESP32 控制器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #fff;
padding: 60px 20px;
}
h1 {
text-align: center;
font-size: 24px;
color: #1f2d3d;
margin-bottom: 40px;
font-weight: 600;
}
.control-btn {
display: block;
width: 220px;
height: 70px;
margin: 0 auto 25px;
border: none;
border-radius: 35px;
font-size: 24px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #ff6b6b, #e74c3c);
box-shadow: 0 8px 20px rgba(231,76,60,0.3);
transition: all 0.15s ease;
}
.control-btn:active {
transform: scale(0.96);
}
.control-btn.on {
background: linear-gradient(135deg, #2ecc71, #27ae60);
box-shadow: 0 8px 20px rgba(46,204,110,0.3);
}
/* 关闭按钮样式 - 仅视觉区分,无清理逻辑 */
.close-btn {
background: linear-gradient(135deg, #6c757d, #343a40);
box-shadow: 0 8px 20px rgba(108,117,125,0.3);
}
#status {
text-align: center;
font-size: 16px;
color: #333;
margin-bottom: 20px;
}
/* 电压显示样式 */
.voltage-box {
text-align: center;
font-size: 22px;
font-weight: bold;
color: #ff5722;
margin-bottom: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
/* 竖直iPhone风格电池图标(带充电闪电) */
.battery-icon {
display: inline-block;
width: 30px;
height: 50px;
border: 3px solid #27ae60;
border-radius: 4px;
border-bottom-width: 3px;
position: relative;
background: linear-gradient(to bottom, rgba(39, 174, 96, 0.1) 0%, rgba(39, 174, 96, 0.2) 100%);
}
/* 电池顶部凸起 */
.battery-icon::before {
content: '';
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
width: 15px;
height: 6px;
background: #27ae60;
border-radius: 3px 3px 0 0;
}
/* 电池内部电量指示 */
.battery-icon::after {
content: '';
position: absolute;
bottom: 3px;
left: 3px;
right: 3px;
height: 85%;
background: linear-gradient(to top, #2ecc71, #27ae60);
border-radius: 1px;
}
/* 充电闪电图标 */
.battery-icon .flash {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(30deg);
width: 18px;
height: 18px;
color: #ffffff;
font-size: 18px;
font-weight: bold;
text-shadow: 0 0 2px rgba(0,0,0,0.2);
z-index: 1;
}
.pwm-box {
padding: 0 10px;
}
.pwm-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 20px;
}
#pwm-value {
font-size: 22px;
font-weight: bold;
color: #007aff;
}
.volume-container {
position: relative;
width: 100%;
height: 70px;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 1px;
}
.bar {
width: 3.5px;
background: #e5e7eb;
border-radius: 2px;
height: 10%;
transition: height 0.05s linear;
}
#pwmSlider {
-webkit-appearance: none;
position: absolute;
width: 100%;
height: 70px;
top: 0;
left: 0;
background: transparent;
outline: none;
z-index: 10;
margin: 0;
}
#pwmSlider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 26px;
height: 26px;
border-radius: 50%;
background: #007aff;
border: 3px solid #fff;
box-shadow: 0 0 12px rgba(0,122,255,0.5);
}
</style>
</head>
<body>
<h1>✨ ESP32 控制器 ✨</h1>
<button id="ledBtn" class="control-btn">点亮LED</button>
<!-- 退出按钮 - 增加清理+关闭逻辑 -->
<button id="closeBtn" class="control-btn close-btn">退出</button>
<div id="status">等待操作...</div>
<!-- 实时电压显示(带竖直iPhone风格充电电池图标) -->
<div class="voltage-box">
<div class="battery-icon">
<span class="flash">⚡</span> <!-- 充电闪电符号 -->
</div>
<div>
实时电压:<span id="volt">0.00</span> V
</div>
</div>
<div class="pwm-box">
<div class="pwm-title">
<span>电机 强度</span>
<span id="pwm-value">750</span>
</div>
<div class="volume-container">
<div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div><div class="bar"></div>
<input type="range" id="pwmSlider" min="0" max="1000" value="100"> <!-- 修正min值为0,匹配退出逻辑 -->
</div>
</div>
<script>
const esp32_ip = "192.168.4.1";
let ledOn = false;
const btn = document.getElementById("ledBtn");
const closeBtn = document.getElementById("closeBtn");
const status = document.getElementById("status");
const slider = document.getElementById("pwmSlider");
const valueText = document.getElementById("pwm-value");
const bars = document.querySelectorAll(".bar");
const voltText = document.getElementById("volt");
// 电压刷新定时器(保留原有逻辑)
let voltageTimer = setInterval(getVoltage, 1000);
// UI更新
function updateUI() {
const val = slider.value;
valueText.textContent = val;
const percentage = (val - 0) / 10; // 修正百分比计算(min改为0)
const totalBars = bars.length;
bars.forEach((bar, index) => {
const heightRatio = index / totalBars;
bar.style.height = 10 + heightRatio * 55 + "%";
const hue = 200 + heightRatio * 40;
const saturation = 100;
const lightness = percentage > heightRatio * 100 ? 60 : 90;
bar.style.backgroundColor = `hsl({hue}, {saturation}%, ${lightness}%)`;
});
}
// 设置PWM
async function setPWM(v) {
try {
await fetch(`http://${esp32_ip}/pwm/${v}`);
status.textContent = `PWM: ${v}`;
} catch (e) {
status.textContent = "连接失败";
}
}
// 获取电压
async function getVoltage() {
try {
let res = await fetch(`http://${esp32_ip}/voltage`);
let volt = await res.text();
voltText.textContent = volt;
} catch (e) {
voltText.textContent = "--";
}
}
// LED控制
btn.addEventListener("click", async () => {
const cmd = ledOn ? "off" : "on";
try {
await fetch(`http://${esp32_ip}/light/${cmd}`);
ledOn = !ledOn;
btn.textContent = ledOn ? "熄灭LED" : "点亮LED";
btn.classList.toggle("on", ledOn);
status.textContent = ledOn ? "LED 已点亮" : "LED 已熄灭";
} catch (e) {
status.textContent = "连接失败";
}
});
// 退出按钮 - 完整逻辑:设置PWM=0 → 清理资源 → 申请权限关闭页面
closeBtn.addEventListener("click", async () => {
try {
// 1. 第一步:设置PWM值为0(发送到py文件)
await setPWM(0);
status.textContent = "正在退出,已设置PWM为0...";
// 2. 第二步:清理页面资源
clearInterval(voltageTimer); // 停止电压刷新定时器
slider.removeEventListener("input", updateUI); // 移除滑块监听
btn.removeEventListener("click", btn.onclick); // 移除LED按钮监听
bars.forEach(bar => bar.style.backgroundColor = "#e5e7eb"); // 重置UI
voltText.textContent = "0.00"; // 重置电压显示
valueText.textContent = "0"; // 重置PWM显示
slider.value = 0; // 重置滑块值
updateUI(); // 更新UI到PWM=0状态
// 3. 第三步:申请系统权限并尝试关闭页面
// 注:浏览器限制,仅在用户主动触发+特定场景下可关闭窗口
const userConfirm = confirm("是否确认关闭页面?\n(需授权才能关闭窗口权限)");
if (userConfirm) {
// 尝试关闭当前窗口(仅对通过window.open打开的窗口/部分浏览器生效)
if (window.close) {
window.close();
} else {
alert("浏览器限制,无法自动关闭页面,请手动关闭!");
}
} else {
status.textContent = "取消退出,已重置PWM为0";
}
} catch (e) {
status.textContent = "退出失败:" + e.message;
}
});
// PWM滑动
slider.addEventListener("input", () => {
updateUI();
setPWM(slider.value);
});
// 初始化
updateUI();
setPWM(slider.value);
getVoltage();
</script>
</body>
</html>
修改后 a.py(完整代码)
import socket
import network
import time
from machine import Pin, PWM, ADC
------------------ 全局配置 ------------------
LED配置 (板载GPIO2)
led = Pin(2, Pin.OUT)
led.value(0)
PWM配置 (引脚14,频率500Hz)
pwm_pin = 14
pwm_freq = 500
pwm = PWM(Pin(pwm_pin), freq=pwm_freq, duty=0)
PWM_MIN = 0
PWM_MAX = 1000
电压检测ADC配置 (GPIO34)
adc = ADC(Pin(34))
adc.atten(ADC.ATTN_11DB)
adc.width(ADC.WIDTH_12BIT)
------------------ 新增:电压读取函数 ------------------
def read_voltage(adc, R1=10000, R2=4700, samples=100):
"""
读取ADC平均值并计算分压后的电压值
-
参数:
-
adc: ADC对象
-
R1: 分压电阻1 (默认10kΩ)
-
R2: 分压电阻2 (默认4.7kΩ)
-
samples: 采样数量 (默认100)
-
返回值: 测量电压(单位: 伏)
"""
try:
total = 0
for _ in range(samples):
total += adc.read()
adc_value = total / samples
divider_ratio = (R1 + R2) / R2
voltage = (adc_value * 3.3 / 4095) * divider_ratio + 0.75 # 补偿+0.55V
return round(voltage, 2)
except Exception as e:
print("读取电压时出错:", e)
return 0.0
------------------ AP热点启动 ------------------
def start_ap():
ap = network.WLAN(network.AP_IF)
ap.active(False)
time.sleep(0.5)
ap.active(True)
ssid = 'ESP32_APTest'
password = '12345678'
ap.config(
essid=ssid,
password=password,
authmode=3,
max_clients=10
)
while not ap.active():
time.sleep(0.1)
print('='*30)
print('AP模式已启动')
print('SSID:', ssid)
print('密码:', password)
ip = ap.ifconfig()[0]
print('IP地址:', ip)
print('PWM控制: http://' + ip + '/pwm/500~1000')
print('电压获取: http://' + ip + '/voltage')
print('='*30)
return ip
------------------ Web服务器(API接口) ------------------
def web_server(ip):
web_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
web_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
web_socket.bind(("0.0.0.0", 80))
web_socket.listen(5)
print("服务器启动完成!")
print("访问示例:")
print(" LED开: http://%s/light/on" % ip)
print(" LED关: http://%s/light/off" % ip)
print(" PWM设值: http://%s/pwm/666 (数值500-1000)" % ip)
print(" 获取电压: http://%s/voltage" % ip)
print("="*40)
while True:
新增:每次循环都检测电压,低于7.2V则停止PWM输出
volt = read_voltage(adc)
if volt < 7.2:
pwm.duty(0) # 强制设为0,停止PWM输出
try:
conn, addr = web_socket.accept()
request = conn.recv(1024).decode("utf-8")
response = ""
if "/light/on" in request:
led.value(1)
response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nLED已点亮"
elif "/light/off" in request:
led.value(0)
response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nLED已熄灭"
elif "/pwm/" in request:
try:
pwm_str = request.split('/pwm/')[1].split(' ')[0]
target_val = int(pwm_str)
if target_val < PWM_MIN:
target_val = PWM_MIN
if target_val > PWM_MAX:
target_val = PWM_MAX
电压低于7.2V时,忽略设置并强制设0
if volt < 7.2:
pwm.duty(0)
response = f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\n当前电压{volt}V < 7.2V,PWM已强制停止(duty=0),忽略设置值{target_val}"
else:
pwm.duty(target_val)
response = f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\nPWM设置成功: {target_val} (当前电压: {volt}V)"
except:
response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\n参数错误!请输入 500-1000 数字"
------------------ 新增:电压API ------------------
elif "/voltage" in request:
volt = read_voltage(adc)
response = f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\n\r\n{volt}"
else:
response = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\nAPI不存在"
conn.send(response.encode("utf-8"))
conn.close()
except:
try:
conn.close()
except:
pass
continue
------------------ 入口 ------------------
def main():
pwm.duty(1023)
time.sleep(2)
pwm.duty(400)
time.sleep(2)
ip = start_ap()
web_server(ip)
if name == "main":
main()