本文介绍如何编写 c++ 程序启动 esp8266 的局域网服务,通过接口调用控制机械臂舵机的操作。同时,利用上篇文章所介绍的静态网站服务,提供 pc 端和移动端控制页面。
套件介绍
试验用机械臂由4个舵机、激光切割的木板、螺丝和螺母组成,不含舵机的机械臂仅售 9.9 元,便宜又好玩。由于要驱动4个舵机运行,根据上次的文章 arduino 操作舵机和供电 舵机需要独立供电,因此我使用一节 18650 电池作为单独供电,电池的电压范围是 3.7-4.2v 正好可以驱动舵机;单片机则使用 typec 接口供电。
pc 端机械臂控制页面
通过监听按键事件,分别为上下左右按键和wasd按键绑定事件逻辑,按下按键是发送相应的网络请求。esp8266接收到对应请求后执行舵机角度旋转。
移动端机械臂控制页面
运行效果
编写机械臂控制程序
卖家没有提供控制代码,而且控制机械臂的单片机多种多样,下面通过编写 c++ 程序为 esp8266 开启局域网服务,控制4个舵机的旋转角度,从而控制机械臂的运作。
C++
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebSrv.h>
#include <LittleFS.h>
#include <Servo.h>
/*
Replace the SSID and Password according to your wifi
*/
#ifndef STASSID
#define STASSID "你的wifi名称"
#define STAPSK "你的wifi密码"
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
Servo s1; // 底盘
Servo s2; // 左边舵机
Servo s3; // 右边舵机
Servo s4; // 夹子
int deg1 = 90;
int deg2 = 90;
int deg3 = 90;
int deg4 = 60; // 夹子60送-40夹紧
int deg4Max = 60;
int deg4Min = 35;
int deg4Default = 60;
// Webserver
AsyncWebServer server(80);
String PARAM_MESSAGE = "status";
const int LED_PIN = D5;
void notFound(AsyncWebServerRequest *request)
{
request->send(404, "text/plain", "Not found");
}
void toggleLED(String status)
{
if (status == "ON")
digitalWrite(LED_PIN, LOW);
else
digitalWrite(LED_PIN, HIGH);
}
void turn(Servo &servo, const String direction, int °, bool is4 = false) {
// 4号夹子特殊逻辑
if (is4) {
if (direction == "left" && deg > deg4Min) {
deg -= 1;
servo.write(deg);
} else if (direction == "right" && deg < deg4Max) {
deg += 1;
servo.write(deg);
} else if (direction == "reset") {
deg = deg4Default;
servo.write(deg);
}
return;
}
if (direction == "left" && deg > 0) {
deg -= 2;
servo.write(deg);
} else if (direction == "right" && deg < 180) {
deg += 2;
servo.write(deg);
} else if (direction == "reset") {
deg = 90;
servo.write(deg);
}
}
void setup()
{
// 舵机
s1.attach(D1);
s2.attach(D2);
s3.attach(D3);
s4.attach(D4);
s1.write(deg1);
s2.write(deg2);
s3.write(deg3);
s4.write(deg4);
Serial.begin(115200);
Serial.println("Starting the LittleFS Webserver..");
// Begin LittleFS
if (!LittleFS.begin())
{
Serial.println("An Error has occurred while mounting LittleFS");
return;
}
// Connect to WIFI
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.printf("WiFi Failed!\n");
return;
}
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
// LED PIN
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
// Route for root index.html
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(LittleFS, "/index.html", "text/html"); });
// Route for root index.css
server.on("/index.css", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(LittleFS, "/index.css", "text/css"); });
// Route for root entireframework.min.css
// server.on("/entireframework.min.css", HTTP_GET, [](AsyncWebServerRequest *request)
// { request->send(LittleFS, "/entireframework.min.css", "text/css"); });
// Route for root index.js
// server.on("/index.js", HTTP_GET, [](AsyncWebServerRequest *request)
// { request->send(LittleFS, "/index.js", "text/javascript"); });
server.on("/s1", HTTP_GET, [](AsyncWebServerRequest *request){
String direction;
String message;
if (request->hasParam("direction")) {
direction = request->getParam("direction")->value();
turn(s1, direction, deg1);
// if (direction == "left" && deg1 > 0) {
// deg1 -= 10;
// s1.write(deg1);
// message = "turn left, now is " + deg1;
// } else if (direction == "right" && deg1 < 180) {
// deg1 += 10;
// s1.write(deg1);
// message = "turn right, now is " + deg1;
// }
// message = "good";
} else {
deg1 = 90;
s1.write(deg1);
message = "error";
}
request->send(200, "text/plain", "servo1: " + message);
});
server.on("/s2", HTTP_GET, [](AsyncWebServerRequest *request){
String direction;
String message;
if (request->hasParam("direction")) {
direction = request->getParam("direction")->value();
turn(s2, direction, deg2);
} else {
message = "need param direction";
}
request->send(200, "text/plain", "servo2: " + message);
});
server.on("/s3", HTTP_GET, [](AsyncWebServerRequest *request){
String direction;
String message;
if (request->hasParam("direction")) {
direction = request->getParam("direction")->value();
turn(s3, direction, deg3);
} else {
message = "need param direction";
}
request->send(200, "text/plain", "servo3: " + message);
});
server.on("/s4", HTTP_GET, [](AsyncWebServerRequest *request){
String direction;
String message;
if (request->hasParam("direction")) {
direction = request->getParam("direction")->value();
turn(s4, direction, deg4, true);
} else {
message = "need param direction";
}
request->send(200, "text/plain", "servo4: " + message);
});
// Respond to toggle event
server.on("/toggle", HTTP_GET, [](AsyncWebServerRequest *request)
{
String status;
if (request->hasParam(PARAM_MESSAGE)) {
status = request->getParam(PARAM_MESSAGE)->value();
if(status == "on"){
toggleLED("ON");
}else{
toggleLED("OFF");
}
} else {
status = "No message sent";
}
request->send(200, "text/plain", "Turning Built In LED : " + status); });
server.onNotFound(notFound);
server.begin();
}
void loop()
{
}
编写前端页面
此页面支持 pwa 添加到手机桌面,更接近原生体验。此处省略 css 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#8ECAE6">
<meta name="apple-mobile-web-app-title" content="esp8266">
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/test.jpg">
<link rel="apple-touch-icon" sizes="120x120" href="/test.jpg">
<title>esp8266</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<!-- <h1 class="title">esp8266</h1> -->
<div class="mobile">
<div class="btn-group">
<div class="btn" data-name="w">前伸</div>
<div class="btn-middle">
<div class="btn" data-name="a">夹紧</div>
<div class="btn" data-name="d">放松</div>
</div>
<div class="btn" data-name="s">后缩</div>
</div>
<div class="btn btn-reset" data-name="reset">重置</div>
<div class="btn-group">
<div class="btn" data-name="up">上抬</div>
<div class="btn-middle">
<div class="btn" data-name="left">左转</div>
<div class="btn" data-name="right">右转</div>
</div>
<div class="btn" data-name="down">下压</div>
</div>
</div>
<div class="pc">
<h2>舵机1底盘转向(← →)</h2>
<div data-sid="s1" class="btn-group">
<button data-action="left" >右转</button>
<button data-action="right" >左转</button>
<button data-action="reset" >重置</button>
</div>
<h2>舵机2上下压(↑ ↓)</h2>
<div data-sid="s2" class="btn-group">
<button data-action="left" >下压</button>
<button data-action="right" >上抬</button>
<button data-action="reset" >重置</button>
</div>
<h2>舵机3前后伸缩(W S)</h2>
<div data-sid="s3" class="btn-group">
<button data-action="left" >后缩</button>
<button data-action="right" >前伸</button>
<button data-action="reset" >重置</button>
</div>
<h2>舵机4夹子(A D)</h2>
<div data-sid="s4" class="btn-group">
<button data-action="left" >夹紧</button>
<button data-action="right" >张开</button>
<button data-action="reset" >重置</button>
</div>
</div>
<script>
function exec(sid, action) {
const url = `/${sid}?direction=${action}`
fetch(url)
}
const keyMap = {
up: {
code: 38,
servo: 's2',
action: 'right'
},
down: {
code: 40,
servo: 's2',
action: 'left'
},
left: {
code: 37,
servo: 's1',
action: 'right'
},
right: {
code: 39,
servo: 's1',
action: 'left'
},
w: {
code: 87,
servo: 's3',
action: 'right'
},
s: {
code: 83,
servo: 's3',
action: 'left'
},
a: {
code: 65,
servo: 's4',
action: 'left'
},
d: {
code: 68,
servo: 's4',
action: 'right'
}
}
var throttle = function(func, delay) {
var prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
const availableCodes = Object.values(keyMap).map(item => item.code)
document.addEventListener('keydown', function (e) {
console.log('keydown', e.keyCode)
if(availableCodes.includes(e.keyCode)) {
handleKeyEvent(true, e.keyCode)
}
})
document.addEventListener('touchstart', function (e) {
for(let touch of e.touches) {
const dataset = touch.target.dataset
if(dataset.name === 'reset') {
fetch('/s1/?direction=reset')
fetch('/s2/?direction=reset')
fetch('/s3/?direction=reset')
fetch('/s4/?direction=reset')
touch.target.classList.add('btn-active')
continue
}
if(dataset.name && !keyMap[dataset.name].interval) {
touch.target.classList.add('btn-active')
const keyItem = keyMap[dataset.name]
keyItem.interval = setInterval((function sendRequest() {
fetch(`/${keyItem.servo}?direction=${keyItem.action}`)
return sendRequest
})(), 100)
}
}
})
document.addEventListener('keyup', function (e) {
console.log('keyup', e.keyCode)
if(availableCodes.includes(e.keyCode)) {
handleKeyEvent(false, e.keyCode)
}
})
document.addEventListener('touchend', function (e) {
const dataset = e.target.dataset
if(dataset.name && keyMap[dataset.name]?.interval) {
clearInterval(keyMap[dataset.name].interval)
keyMap[dataset.name].interval = null
}
e.target.classList.remove('btn-active')
})
function handleKeyEvent(isDown, keyCode) {
const keyItem = Object.values(keyMap).find(item => item.code === keyCode)
if(isDown && !keyItem.interval) {
keyItem.interval = setInterval((function sendRequest() {
fetch(`/${keyItem.servo}?direction=${keyItem.action}`)
return sendRequest
})(), 100)
} else if(!isDown) {
clearInterval(keyItem.interval)
keyItem.interval = null
}
}
</script>
</body>
</html>