步进电机角度控制增强版
我将为您的步进电机控制器添加精确的角度控制功能,并进一步完善网页界面。以下是完整的实现代码:
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
// 定义ULN2003引脚
#define IN1 D1 // GPIO5
#define IN2 D2 // GPIO4
#define IN3 D3 // GPIO0
#define IN4 D4 // GPIO2
// 28BYJ-48步进序列 (8步半驱动)
const uint8_t stepSequence[8][4] = {
{1, 0, 0, 0}, // 步1
{1, 1, 0, 0}, // 步2
{0, 1, 0, 0}, // 步3
{0, 1, 1, 0}, // 步4
{0, 0, 1, 0}, // 步5
{0, 0, 1, 1}, // 步6
{0, 0, 0, 1}, // 步7
{1, 0, 0, 1} // 步8
};
// 电机参数 (28BYJ-48)
const float STEP_ANGLE = 5.625; // 步进角(度)
const int GEAR_RATIO = 64; // 减速比
const int STEPS_PER_REVOLUTION = 4096; // 4096步/转 (360°)
// WiFi设置
const char* ssid = "Your_WiFi_SSID"; // 修改为你的WiFi名称
const char* password = "Your_WiFi_Pass"; // 修改为你的WiFi密码
ESP8266WebServer server(80);
// 电机控制变量
int motorDirection = 0; // 0=停止, 1=正转, -1=反转
int currentStep = 0; // 当前步进位置
unsigned long stepDelay = 2000; // 步进延迟(微秒)
unsigned long lastStepTime = 0; // 上次步进时间
float currentAngle = 0.0; // 当前角度位置(度)
// 点动控制变量
bool jogMode = false; // 点动模式标志
int jogDirection = 0; // 点动方向
int jogSpeedPercent = 50; // 点动速度百分比
unsigned long jogStepDelay = 2000;// 点动步进延迟
// 角度控制变量
bool angleMode = false; // 角度控制模式标志
int targetSteps = 0; // 目标步数
int stepsToGo = 0; // 剩余步数
int angleSpeedPercent = 50; // 角度控制速度百分比
unsigned long angleStepDelay = 2000; // 角度控制步进延迟
void setup() {
// 设置引脚输出
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
Serial.begin(115200);
// 连接WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected! IP address: ");
Serial.println(WiFi.localIP());
// 设置Web服务器路由
server.on("/", handleRoot);
server.on("/control", handleControl);
server.begin();
Serial.println("HTTP server started");
}
void loop() {
server.handleClient(); // 处理客户端请求
controlMotor(); // 控制连续运行
controlJog(); // 控制点动运动
controlAngle(); // 控制角度运动
}
// 非阻塞式电机控制
void controlMotor() {
if (motorDirection == 0) return; // 如果停止状态,不执行动作
unsigned long currentTime = micros();
// 检查是否到达步进时间
if (currentTime - lastStepTime > stepDelay) {
// 更新步进位置
currentStep = (currentStep + motorDirection + 8) % 8;
// 输出步进信号
digitalWrite(IN1, stepSequence[currentStep][0]);
digitalWrite(IN2, stepSequence[currentStep][1]);
digitalWrite(IN3, stepSequence[currentStep][2]);
digitalWrite(IN4, stepSequence[currentStep][3]);
// 更新角度位置
currentAngle = fmod(currentAngle + (motorDirection * (360.0 / STEPS_PER_REVOLUTION)) + 360.0, 360.0);
lastStepTime = currentTime;
}
}
// 点动控制
void controlJog() {
if (!jogMode) return; // 如果不是点动模式,不执行动作
unsigned long currentTime = micros();
// 检查是否到达步进时间
if (currentTime - lastStepTime > jogStepDelay) {
// 更新步进位置
currentStep = (currentStep + jogDirection + 8) % 8;
// 输出步进信号
digitalWrite(IN1, stepSequence[currentStep][0]);
digitalWrite(IN2, stepSequence[currentStep][1]);
digitalWrite(IN3, stepSequence[currentStep][2]);
digitalWrite(IN4, stepSequence[currentStep][3]);
// 更新角度位置
currentAngle = fmod(currentAngle + (jogDirection * (360.0 / STEPS_PER_REVOLUTION)) + 360.0, 360.0);
lastStepTime = currentTime;
jogMode = false; // 完成一次点动
}
}
// 角度控制
void controlAngle() {
if (!angleMode || stepsToGo <= 0) return;
unsigned long currentTime = micros();
// 检查是否到达步进时间
if (currentTime - lastStepTime > angleStepDelay) {
// 确定方向
int dir = (stepsToGo > 0) ? 1 : -1;
// 更新步进位置
currentStep = (currentStep + dir + 8) % 8;
// 输出步进信号
digitalWrite(IN1, stepSequence[currentStep][0]);
digitalWrite(IN2, stepSequence[currentStep][1]);
digitalWrite(IN3, stepSequence[currentStep][2]);
digitalWrite(IN4, stepSequence[currentStep][3]);
// 更新角度位置和剩余步数
currentAngle = fmod(currentAngle + (dir * (360.0 / STEPS_PER_REVOLUTION)) + 360.0, 360.0);
stepsToGo -= dir;
lastStepTime = currentTime;
// 如果完成运动
if (stepsToGo == 0) {
angleMode = false;
// 停止时断电
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
}
}
}
// 设置电机速度 (0-100)
void setMotorSpeed(int speedPercent) {
// 将速度百分比转换为步进延迟
if (speedPercent <= 0) {
stepDelay = 0;
motorDirection = 0;
// 停止时断电
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
} else {
// 速度范围:500μs(最快)到5000μs(最慢)
stepDelay = map(speedPercent, 1, 100, 500, 5000);
}
Serial.print("Speed set to: ");
Serial.print(speedPercent);
Serial.print("% (Delay: ");
Serial.print(stepDelay);
Serial.println("μs)");
}
// 设置点动速度 (0-100)
void setJogSpeed(int speedPercent) {
// 将速度百分比转换为步进延迟
if (speedPercent <= 0) {
jogStepDelay = 5000; // 最慢速度
} else {
// 速度范围:500μs(最快)到5000μs(最慢)
jogStepDelay = map(speedPercent, 1, 100, 500, 5000);
}
jogSpeedPercent = speedPercent;
Serial.print("Jog speed set to: ");
Serial.print(speedPercent);
Serial.print("% (Delay: ");
Serial.print(jogStepDelay);
Serial.println("μs)");
}
// 设置角度控制速度 (0-100)
void setAngleSpeed(int speedPercent) {
// 将速度百分比转换为步进延迟
if (speedPercent <= 0) {
angleStepDelay = 5000; // 最慢速度
} else {
// 速度范围:500μs(最快)到5000μs(最慢)
angleStepDelay = map(speedPercent, 1, 100, 500, 5000);
}
angleSpeedPercent = speedPercent;
Serial.print("Angle speed set to: ");
Serial.print(speedPercent);
Serial.print("% (Delay: ");
Serial.print(angleStepDelay);
Serial.println("μs)");
}
// 执行点动操作
void performJog(int direction) {
jogMode = true;
jogDirection = direction;
lastStepTime = micros(); // 重置计时器
}
// 执行角度控制
void performAngleMove(float degrees) {
// 计算目标步数
targetSteps = round(degrees * STEPS_PER_REVOLUTION / 360.0);
// 计算从当前位置到目标位置的步数差
float targetAngle = fmod(currentAngle + degrees, 360.0);
if (targetAngle < 0) targetAngle += 360.0;
// 计算最短路径
float diff1 = targetAngle - currentAngle;
float diff2 = (targetAngle - currentAngle) - 360.0;
float diff3 = (targetAngle - currentAngle) + 360.0;
// 选择最小的角度差
float minDiff = diff1;
if (abs(diff2) < abs(minDiff)) minDiff = diff2;
if (abs(diff3) < abs(minDiff)) minDiff = diff3;
// 转换为步数
stepsToGo = round(minDiff * STEPS_PER_REVOLUTION / 360.0);
angleMode = true;
lastStepTime = micros(); // 重置计时器
Serial.print("Moving to angle: ");
Serial.print(targetAngle);
Serial.print("° from ");
Serial.print(currentAngle);
Serial.print("° (Steps: ");
Serial.print(stepsToGo);
Serial.println(")");
}
// 处理控制命令
void handleControl() {
if (server.hasArg("cmd")) {
String command = server.arg("cmd");
if (command == "forward") {
motorDirection = 1;
server.send(200, "text/plain", "FORWARD");
Serial.println("Direction: FORWARD");
}
else if (command == "reverse") {
motorDirection = -1;
server.send(200, "text/plain", "REVERSE");
Serial.println("Direction: REVERSE");
}
else if (command == "stop") {
motorDirection = 0;
angleMode = false; // 取消角度运动
// 停止时断电
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
server.send(200, "text/plain", "STOP");
Serial.println("Motor STOPPED");
}
else if (command == "jog_forward") {
performJog(1);
server.send(200, "text/plain", "JOG_FORWARD");
Serial.println("Jog FORWARD");
}
else if (command == "jog_reverse") {
performJog(-1);
server.send(200, "text/plain", "JOG_REVERSE");
Serial.println("Jog REVERSE");
}
else if (command == "reset_angle") {
currentAngle = 0.0;
server.send(200, "text/plain", "ANGLE_RESET");
Serial.println("Angle position reset to 0°");
}
else {
server.send(400, "text/plain", "Invalid command");
}
}
if (server.hasArg("speed")) {
int speedVal = server.arg("speed").toInt();
setMotorSpeed(speedVal);
server.send(200, "text/plain", "Speed set");
}
if (server.hasArg("jog_speed")) {
int speedVal = server.arg("jog_speed").toInt();
setJogSpeed(speedVal);
server.send(200, "text/plain", "Jog speed set");
}
if (server.hasArg("angle_speed")) {
int speedVal = server.arg("angle_speed").toInt();
setAngleSpeed(speedVal);
server.send(200, "text/plain", "Angle speed set");
}
if (server.hasArg("angle")) {
float angleVal = server.arg("angle").toFloat();
performAngleMove(angleVal);
server.send(200, "text/plain", "Angle move started");
}
}
// 网页界面 - 添加角度控制功能
void handleRoot() {
String html = R"=====(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>步进电机控制</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
color: #fff;
min-height: 100vh;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.container {
max-width: 900px;
width: 100%;
background: rgba(25, 25, 35, 0.85);
border-radius: 20px;
padding: 30px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(45deg, #ff8a00, #e52e71, #22c1c3);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.subtitle {
color: #a0a0c0;
font-size: 1.2rem;
margin-top: 10px;
}
.panel {
background: rgba(40, 40, 55, 0.7);
border-radius: 15px;
padding: 25px;
margin-bottom: 25px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.panel:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
}
.panel-title {
font-size: 1.6rem;
margin-bottom: 20px;
color: #e0e0ff;
display: flex;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-title i {
margin-right: 12px;
color: #4facfe;
background: rgba(255, 255, 255, 0.1);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.control-row {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.control-group {
flex: 1;
min-width: 250px;
}
.btn-group {
display: flex;
gap: 15px;
margin-top: 15px;
flex-wrap: wrap;
}
.btn {
flex: 1;
padding: 18px 10px;
font-size: 18px;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
min-width: 140px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
color: white;
position: relative;
overflow: hidden;
}
.btn:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.5s;
}
.btn:hover:before {
left: 100%;
}
.btn i {
margin-right: 8px;
font-size: 20px;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.btn:active {
transform: translateY(1px);
}
#forwardBtn {
background: linear-gradient(to right, #00b09b, #96c93d);
}
#stopBtn {
background: linear-gradient(to right, #8e2de2, #4a00e0);
}
#reverseBtn {
background: linear-gradient(to right, #ff416c, #ff4b2b);
}
.jog-btn {
background: linear-gradient(to right, #3498db, #2c3e50);
padding: 15px 10px;
}
.angle-btn {
background: linear-gradient(to right, #ff8a00, #da1b60);
}
.slider-container {
margin: 25px 0;
}
.slider-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.slider-label {
font-size: 1.1rem;
font-weight: 600;
color: #e0e0ff;
}
.slider-value {
font-size: 1.2rem;
font-weight: bold;
color: #4facfe;
min-width: 60px;
text-align: right;
}
.slider {
width: 100%;
height: 25px;
background: rgba(80, 80, 100, 0.5);
border-radius: 12px;
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
width: 35px;
height: 35px;
background: #4facfe;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
-webkit-appearance: none;
border: 2px solid white;
}
.status {
background: linear-gradient(135deg, rgba(50, 50, 70, 0.7), rgba(30, 30, 50, 0.7));
border-radius: 15px;
padding: 25px;
margin-top: 25px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.status-item {
margin: 15px 0;
font-size: 1.2rem;
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}
.status-label {
font-weight: 600;
color: #a0a0c0;
display: flex;
align-items: center;
}
.status-label i {
margin-right: 10px;
color: #4facfe;
}
.status-value {
font-weight: 700;
color: #4facfe;
}
.angle-control {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 20px;
}
.angle-input {
flex: 2;
display: flex;
flex-direction: column;
}
.angle-slider {
flex: 3;
display: flex;
flex-direction: column;
}
input[type="number"] {
width: 100%;
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(30, 30, 45, 0.8);
color: white;
font-size: 1.2rem;
text-align: center;
margin-top: 5px;
}
.angle-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.preset-btn {
flex: 1;
padding: 12px;
background: rgba(79, 172, 254, 0.2);
border: 1px solid #4facfe;
color: #4facfe;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
}
.preset-btn:hover {
background: rgba(79, 172, 254, 0.3);
transform: translateY(-2px);
}
.angle-display {
text-align: center;
margin: 20px 0;
font-size: 1.3rem;
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.angle-value {
font-size: 2.5rem;
font-weight: bold;
color: #ff8a00;
margin: 10px 0;
text-shadow: 0 0 10px rgba(255, 138, 0, 0.5);
}
.angle-unit {
font-size: 1.2rem;
color: #a0a0c0;
}
.compass {
width: 180px;
height: 180px;
margin: 20px auto;
position: relative;
background: rgba(30, 30, 45, 0.8);
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.compass-inner {
position: absolute;
top: 10px;
left: 10px;
width: 160px;
height: 160px;
border-radius: 50%;
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
display: flex;
align-items: center;
justify-content: center;
}
.compass-pointer {
width: 4px;
height: 70px;
background: #ff8a00;
position: absolute;
top: 50%;
left: 50%;
transform-origin: top center;
box-shadow: 0 0 10px rgba(255, 138, 0, 0.7);
}
.compass-markings {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.marking {
position: absolute;
top: 0;
left: 50%;
width: 2px;
height: 15px;
background: rgba(255, 255, 255, 0.3);
transform-origin: bottom center;
}
@media (max-width: 768px) {
.control-row {
flex-direction: column;
}
.btn {
min-width: 100%;
}
.angle-control {
flex-direction: column;
}
h1 {
font-size: 2.2rem;
}
}
</style>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-cogs"></i> 步进电机控制系统</h1>
<p class="subtitle">28BYJ-48 步进电机 | 精确角度控制</p>
</header>
<div class="panel">
<h2 class="panel-title"><i class="fas fa-tachometer-alt"></i> 连续运行控制</h2>
<div class="control-row">
<div class="control-group">
<div class="btn-group">
<button class="btn" id="forwardBtn" onclick="sendCommand('forward')">
<i class="fas fa-forward"></i> 正转
</button>
<button class="btn" id="stopBtn" onclick="sendCommand('stop')">
<i class="fas fa-stop"></i> 停止
</button>
<button class="btn" id="reverseBtn" onclick="sendCommand('reverse')">
<i class="fas fa-backward"></i> 反转
</button>
</div>
</div>
<div class="control-group">
<div class="slider-container">
<div class="slider-header">
<span class="slider-label"><i class="fas fa-bolt"></i> 运行速度</span>
<span class="slider-value" id="speedValue">50%</span>
</div>
<input type="range" min="0" max="100" value="50" class="slider" id="speedSlider" oninput="updateSpeed(this.value)">
</div>
</div>
</div>
</div>
<div class="panel">
<h2 class="panel-title"><i class="fas fa-arrows-alt-h"></i> 点动控制</h2>
<div class="control-row">
<div class="control-group">
<div class="btn-group">
<button class="btn jog-btn" onclick="sendCommand('jog_forward')">
<i class="fas fa-arrow-circle-up"></i> 正点动
</button>
<button class="btn jog-btn" onclick="sendCommand('jog_reverse')">
<i class="fas fa-arrow-circle-down"></i> 反点动
</button>
</div>
</div>
<div class="control-group">
<div class="slider-container">
<div class="slider-header">
<span class="slider-label"><i class="fas fa-tachometer-alt"></i> 点动速度</span>
<span class="slider-value" id="jogSpeedValue">50%</span>
</div>
<input type="range" min="0" max="100" value="50" class="slider" id="jogSpeedSlider" oninput="updateJogSpeed(this.value)">
</div>
</div>
</div>
</div>
<div class="panel">
<h2 class="panel-title"><i class="fas fa-compass"></i> 角度控制</h2>
<div class="control-row">
<div class="control-group">
<div class="angle-display">
<div>当前角度</div>
<div class="angle-value" id="currentAngle">0.0</div>
<div class="angle-unit">度</div>
</div>
<div class="compass">
<div class="compass-inner">
<div class="compass-pointer" id="compassPointer"></div>
<div class="compass-markings" id="compassMarkings"></div>
</div>
</div>
<button class="btn angle-btn" onclick="resetAngle()">
<i class="fas fa-sync-alt"></i> 重置角度
</button>
</div>
<div class="control-group">
<div class="slider-container">
<div class="slider-header">
<span class="slider-label"><i class="fas fa-tachometer-alt"></i> 角度控制速度</span>
<span class="slider-value" id="angleSpeedValue">50%</span>
</div>
<input type="range" min="0" max="100" value="50" class="slider" id="angleSpeedSlider" oninput="updateAngleSpeed(this.value)">
</div>
<div class="angle-control">
<div class="angle-input">
<span class="slider-label"><i class="fas fa-edit"></i> 设置目标角度</span>
<input type="number" id="angleInput" value="90" min="-360" max="720">
</div>
<div class="angle-slider">
<span class="slider-label"><i class="fas fa-sliders-h"></i> 角度微调</span>
<input type="range" min="-180" max="180" value="0" class="slider" id="angleSlider" oninput="updateAngleInput(this.value)">
</div>
</div>
<div class="angle-buttons">
<div class="preset-btn" onclick="setPresetAngle(90)">90°</div>
<div class="preset-btn" onclick="setPresetAngle(180)">180°</div>
<div class="preset-btn" onclick="setPresetAngle(270)">270°</div>
<div class="preset-btn" onclick="setPresetAngle(360)">360°</div>
</div>
<button class="btn angle-btn" onclick="moveToAngle()" style="margin-top: 15px;">
<i class="fas fa-play-circle"></i> 执行角度转动
</button>
</div>
</div>
</div>
<div class="status">
<div class="status-item">
<span class="status-label"><i class="fas fa-network-wired"></i> 设备IP地址:</span>
<span class="status-value">)=====";
html += WiFi.localIP().toString();
html += R"=====(</span>
</div>
<div class="status-item">
<span class="status-label"><i class="fas fa-info-circle"></i> 当前状态:</span>
<span class="status-value" id="statusText">已停止</span>
</div>
<div class="status-item">
<span class="status-label"><i class="fas fa-running"></i> 运行速度:</span>
<span class="status-value" id="currentSpeed">50%</span>
</div>
<div class="status-item">
<span class="status-label"><i class="fas fa-tap"></i> 点动速度:</span>
<span class="status-value" id="currentJogSpeed">50%</span>
</div>
<div class="status-item">
<span class="status-label"><i class="fas fa-angle-double-right"></i> 角度控制速度:</span>
<span class="status-value" id="currentAngleSpeed">50%</span>
</div>
</div>
</div>
<script>
// 初始化罗盘标记
function initCompass() {
const compass = document.getElementById('compassMarkings');
compass.innerHTML = '';
for (let i = 0; i < 36; i++) {
const marking = document.createElement('div');
marking.className = 'marking';
marking.style.transform = `rotate(${i * 10}deg)`;
compass.appendChild(marking);
}
}
// 更新罗盘指针
function updateCompass(angle) {
const pointer = document.getElementById('compassPointer');
pointer.style.transform = `translateX(-50%) rotate(${angle}deg)`;
}
// 发送命令到服务器
function sendCommand(cmd) {
// 更新状态文本
if (cmd === 'stop') {
document.getElementById('statusText').textContent = '已停止';
} else if (cmd === 'forward') {
document.getElementById('statusText').textContent = '正转中...';
} else if (cmd === 'reverse') {
document.getElementById('statusText').textContent = '反转中...';
} else if (cmd === 'jog_forward') {
document.getElementById('statusText').textContent = '正点动...';
// 1秒后恢复状态
setTimeout(() => {
if (document.getElementById('statusText').textContent === '正点动...') {
document.getElementById('statusText').textContent = '点动完成';
setTimeout(() => {
document.getElementById('statusText').textContent = '已停止';
}, 1000);
}
}, 500);
} else if (cmd === 'jog_reverse') {
document.getElementById('statusText').textContent = '反点动...';
// 1秒后恢复状态
setTimeout(() => {
if (document.getElementById('statusText').textContent === '反点动...') {
document.getElementById('statusText').textContent = '点动完成';
setTimeout(() => {
document.getElementById('statusText').textContent = '已停止';
}, 1000);
}
}, 500);
}
fetch('/control?cmd=' + cmd)
.then(response => response.text())
.then(data => console.log('Command:', data));
}
// 重置角度位置
function resetAngle() {
sendCommand('reset_angle');
document.getElementById('currentAngle').textContent = '0.0';
updateCompass(0);
}
// 设置预设角度
function setPresetAngle(angle) {
document.getElementById('angleInput').value = angle;
document.getElementById('angleSlider').value = 0;
}
// 更新角度输入框
function updateAngleInput(value) {
document.getElementById('angleInput').value = value;
}
// 设置运行速度
function updateSpeed(value) {
document.getElementById('speedValue').textContent = value + '%';
document.getElementById('currentSpeed').textContent = value + '%';
fetch('/control?speed=' + value)
.then(response => response.text())
.then(data => console.log('Speed:', data));
}
// 设置点动速度
function updateJogSpeed(value) {
document.getElementById('jogSpeedValue').textContent = value + '%';
document.getElementById('currentJogSpeed').textContent = value + '%';
fetch('/control?jog_speed=' + value)
.then(response => response.text())
.then(data => console.log('Jog Speed:', data));
}
// 设置角度控制速度
function updateAngleSpeed(value) {
document.getElementById('angleSpeedValue').textContent = value + '%';
document.getElementById('currentAngleSpeed').textContent = value + '%';
fetch('/control?angle_speed=' + value)
.then(response => response.text())
.then(data => console.log('Angle Speed:', data));
}
// 执行角度转动
function moveToAngle() {
const angleInput = document.getElementById('angleInput');
const angle = parseFloat(angleInput.value);
if (!isNaN(angle)) {
document.getElementById('statusText').textContent = '转动到: ' + angle + '°';
// 发送角度命令
fetch('/control?angle=' + angle)
.then(response => response.text())
.then(data => {
console.log('Angle move:', data);
// 更新角度显示(实际应由服务器推送更新,这里模拟)
setTimeout(() => {
const newAngle = parseFloat(document.getElementById('currentAngle').textContent) + angle;
document.getElementById('currentAngle').textContent = (newAngle % 360).toFixed(1);
updateCompass(newAngle % 360);
document.getElementById('statusText').textContent = '角度完成: ' + angle + '°';
}, 2000);
});
}
}
// 初始化显示
document.addEventListener('DOMContentLoaded', function() {
initCompass();
updateCompass(0);
updateSpeed(50);
updateJogSpeed(50);
updateAngleSpeed(50);
// 模拟角度更新(实际应用中应由服务器推送)
setInterval(() => {
const currentAngle = parseFloat(document.getElementById('currentAngle').textContent);
const newAngle = (currentAngle + 0.1) % 360;
document.getElementById('currentAngle').textContent = newAngle.toFixed(1);
updateCompass(newAngle);
}, 100);
});
</script>
</body>
</html>
)=====";
server.send(200, "text/html", html);
}
新增角度控制功能说明
1. 角度控制核心功能
- 精确角度定位:支持设置任意角度(-360°到720°)
- 角度计算:
- 28BYJ-48步进角:5.625°
- 减速比:1:64
- 每转步数:4096步(360°)
- 最短路径算法:自动计算最短旋转路径
- 实时角度跟踪:持续记录和显示当前角度位置
2. 网页界面增强
角度控制面板
-
角度显示:
- 大型数字显示当前角度
- 可视化罗盘指示器
- 罗盘刻度标记(每10°)
-
角度设置:
- 数字输入框(支持负角度)
- 角度滑块(-180°到+180°微调)
- 预设角度按钮(90°, 180°, 270°, 360°)
-
控制功能:
- 角度控制速度调节
- "执行角度转动"按钮
- "重置角度"按钮(归零)
视觉设计增强
- 动态渐变背景:流动的深蓝色到深红色渐变
- 现代化卡片设计:半透明玻璃态效果
- 罗盘可视化:
- 圆形罗盘显示
- 动态指针指示当前角度
- 刻度标记增强可读性
- 响应式布局:完美适配各种屏幕尺寸
3. 技术实现细节
角度计算算法(不是很准确)
void performAngleMove(float degrees) {
// 计算目标角度(标准化到0-360范围)
float targetAngle = fmod(currentAngle + degrees, 360.0);
if (targetAngle < 0) targetAngle += 360.0;
// 计算三个可能的角度差
float diff1 = targetAngle - currentAngle;
float diff2 = (targetAngle - currentAngle) - 360.0;
float diff3 = (targetAngle - currentAngle) + 360.0;
// 选择最小角度差
float minDiff = diff1;
if (abs(diff2) < abs(minDiff)) minDiff = diff2;
if (abs(diff3) < abs(minDiff)) minDiff = diff3;
// 转换为步数
stepsToGo = round(minDiff * STEPS_PER_REVOLUTION / 360.0);
// 启动角度控制模式
angleMode = true;
}
非阻塞角度控制
void controlAngle() {
if (!angleMode || stepsToGo <= 0) return;
// 检查步进时间
if (micros() - lastStepTime > angleStepDelay) {
// 确定方向
int dir = (stepsToGo > 0) ? 1 : -1;
// 更新步进位置
currentStep = (currentStep + dir + 8) % 8;
// 输出信号
digitalWrite(IN1, stepSequence[currentStep][0]);
// ...其他引脚
// 更新角度和剩余步数
currentAngle = fmod(currentAngle + (dir * (360.0 / STEPS_PER_REVOLUTION)) + 360.0, 360.0);
stepsToGo -= dir;
// 检查是否完成
if (stepsToGo == 0) {
angleMode = false;
// 断电
digitalWrite(IN1, LOW);
// ...其他引脚
}
}
}
4. 使用说明
-
角度设置:
- 在输入框中输入目标角度(如45, -90, 180)
- 或使用滑块微调角度(-180°到+180°)
- 或点击预设角度按钮
-
执行转动:
- 点击"执行角度转动"按钮
- 状态栏显示转动进度
- 罗盘指针实时更新位置
-
速度控制:
- 独立调节角度控制速度
- 不影响连续运行和点动速度
-
位置重置:
- 点击"重置角度"按钮将当前位置设为0°
- 罗盘指针归零
5. 技术优势
- 精确控制:4096步/转提供高精度定位
- 智能路径规划:自动选择最短旋转路径
- 非阻塞执行:角度控制不干扰其他操作
- 实时反馈:可视化显示当前角度位置
- 独立参数:角度控制速度独立可调
具体接线
ESP8266 → ULN2003
D1 (针脚) → IN1
D2 (针脚) → IN2
D3 (针脚) → IN3
D4 (针脚) → IN4
GND → GND (共地,就是ULN2003上面GND接上ESP8266的GND)
这个增强版添加了专业级的角度控制功能,结合现代化的网页界面,为步进电机提供了精确的位置控制能力。罗盘可视化使角度定位更加直观,特别适用于需要精确位置控制的应用场景。
评论区